diff --git a/conftest.py b/conftest.py index ce6a4bb5f..3cd72a36e 100644 --- a/conftest.py +++ b/conftest.py @@ -175,10 +175,19 @@ def pytest_configure(config): def pytest_collection_modifyitems(config, items): + marked_items = [ + (item, item.get_closest_marker("supported_devices")) for item in items + ] + marked_items = [(item, marker) for item, marker in marked_items if marker] + if not marked_items: + # Nothing collected needs the NPU. Resolving one here would open the + # single-tenant device at collection time, contending with whatever + # else holds it and erroring out when none is attached. + return + device = aie_utils.DefaultNPURuntime.device().resolve().name - for item in items: - marker = item.get_closest_marker("supported_devices") - if marker and device not in marker.args: + for item, marker in marked_items: + if device not in marker.args: item.add_marker( pytest.mark.skip( reason=f"Not supported on {device} (supported: {', '.join(marker.args)})" diff --git a/iron/tests/infrastructure/conftest_lazy_device.py b/iron/tests/infrastructure/conftest_lazy_device.py new file mode 100644 index 000000000..3ea80fcc4 --- /dev/null +++ b/iron/tests/infrastructure/conftest_lazy_device.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (C) 2026 Advanced Micro Devices, Inc. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""The root conftest.py's pytest_collection_modifyitems must not resolve a +device unless some collected test restricts itself to specific devices via +@pytest.mark.supported_devices. Resolving one unconditionally opens the +single-tenant NPU on every plain `pytest` in this tree, whatever was selected. + +pytest loads the root conftest.py for these tests too, so the hook under test +is imported by path instead and called directly, against fake items and a +stubbed aie_utils.DefaultNPURuntime that raises if .device() is reached. +""" + +import importlib.util +import sys +from pathlib import Path +from types import SimpleNamespace + +import pytest + +_ROOT_CONFTEST = Path(__file__).resolve().parents[3] / "conftest.py" + + +def _load_root_conftest(): + spec = importlib.util.spec_from_file_location( + "_root_conftest_under_test", _ROOT_CONFTEST + ) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +class _FakeMarker: + def __init__(self, *args): + self.args = args + + +class _FakeItem: + def __init__(self, marker=None): + self._marker = marker + self.markers_added = [] + + def get_closest_marker(self, name): + assert name == "supported_devices" + return self._marker + + def add_marker(self, marker): + self.markers_added.append(marker) + + +class _DeviceCalledError(Exception): + pass + + +def _stub_runtime_that_forbids_device_calls(root_conftest, monkeypatch): + def _raise(): + raise _DeviceCalledError( + "DefaultNPURuntime.device() was called with no marked test collected" + ) + + monkeypatch.setattr( + root_conftest.aie_utils, + "DefaultNPURuntime", + SimpleNamespace(device=_raise), + ) + + +def test_no_device_probe_when_nothing_is_device_restricted(monkeypatch): + root_conftest = _load_root_conftest() + _stub_runtime_that_forbids_device_calls(root_conftest, monkeypatch) + + items = [_FakeItem(), _FakeItem(), _FakeItem()] + root_conftest.pytest_collection_modifyitems(config=None, items=items) + assert all(item.markers_added == [] for item in items) + + +def test_device_probed_and_unsupported_items_skipped_when_a_test_is_restricted( + monkeypatch, +): + root_conftest = _load_root_conftest() + + class _FakeDevice: + def resolve(self): + return SimpleNamespace(name="npu2") + + monkeypatch.setattr( + root_conftest.aie_utils, + "DefaultNPURuntime", + SimpleNamespace(device=lambda: _FakeDevice()), + ) + + unrestricted = _FakeItem() + matches_device = _FakeItem(_FakeMarker("npu1", "npu2")) + excludes_device = _FakeItem(_FakeMarker("npu1")) + + root_conftest.pytest_collection_modifyitems( + config=None, items=[unrestricted, matches_device, excludes_device] + ) + + assert unrestricted.markers_added == [] + assert matches_device.markers_added == [] + assert len(excludes_device.markers_added) == 1 + assert excludes_device.markers_added[0].name == "skip"