Files
computer-playbook/tests/unit/python/plugins/lookup/test_gpu.py
T
kevinveenbirkenbachandClaude Opus 5.5 137afc60cc fix(sys-svc-container): require device and driver for a GPU host, refuse a driverless bare host
Follow-up to ae259a9f58 after an independent review:

- The 136-character task name broke the 80-character name lint
  (tests/lint/repository/test_name_emoji_and_length.py). Names are short
  again; the device/driver/host matrix lives in the role README.
- Both stats now always run and lookup('gpu') requires both registers,
  instead of reading a skipped device stat as "no device".
- A bare host that shows /dev/nvidia0 without nvidia-smi stops the
  deploy with a message naming the fix. It used to die on the missing
  binary anyway; ae259a9f58 would have run its GPU services on the CPU
  silently. A container in that state (a swarm lab node, the dev
  container without its GPU override) still runs on the CPU.
- svc-ai-s1 decided its CUDA build from its own stat of /dev/nvidia0 and
  nvidia-container-runtime while its runtime came from lookup('gpu'), so
  a host with the toolkit but no nvidia-smi would have built a CUDA image
  that gets no GPU. S1_GPU now reads lookup('gpu'), the one detection.
- The libretranslate-engine README and the s1 TODO describe the new
  detection.

test_gpu_detection walks the real tasks over device, driver and
container; against the original 00_core.yml it fails in the
driverless-container, driverless-bare and device-and-driver cells.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 11:57:24 +01:00

125 lines
4.2 KiB
Python

"""Unit tests for plugins/lookup/gpu.py.
Mocks the Ansible-side machinery (loader, lookup_loader, templar) and pins
that the lookup answers True only when the declaration AND the host fact
agree. The declaration resolution itself belongs to resource_filter and is
covered by tests.unit.python.plugins.filter.test_resource_filter.
"""
from __future__ import annotations
import unittest
import unittest.mock as mock
from typing import ClassVar
from ansible.errors import AnsibleError
from plugins.lookup.gpu import LookupModule
APPLICATIONS: ClassVar[dict] = {
"web-svc-libretranslate": {"services": {"libretranslate": {"gpu": True}}},
"web-app-plain": {"services": {"plain": {"gpu": False}}},
"web-app-stringy": {"services": {"stringy": {"gpu": "false"}}},
"web-app-truthy": {"services": {"truthy": {"gpu": "yes"}}},
}
PRESENT: ClassVar[dict] = {
"sys_svc_container_nvidia_device": {"stat": {"exists": True}},
"sys_svc_container_nvidia_driver": {"stat": {"exists": True}},
}
ABSENT: ClassVar[dict] = {
"sys_svc_container_nvidia_device": {"stat": {"exists": False}},
"sys_svc_container_nvidia_driver": {"stat": {"exists": True}},
}
DRIVERLESS: ClassVar[dict] = {
"sys_svc_container_nvidia_device": {"stat": {"exists": True}},
"sys_svc_container_nvidia_driver": {"stat": {"exists": False}},
}
class _Templar:
def __init__(self, vars_=None):
self.available_variables = vars_ or {}
def template(self, value, **_):
return value
def _make_lookup(vars_=None):
lm = LookupModule()
lm._templar = _Templar(vars_ or {})
lm._loader = mock.MagicMock()
return lm
def _run(lm, terms):
loaded = mock.MagicMock()
loaded.run.return_value = [APPLICATIONS]
with mock.patch("plugins.lookup.gpu.lookup_loader") as loader:
loader.get.return_value = loaded
return lm.run(terms, variables=lm._templar.available_variables)
class TestGpuLookup(unittest.TestCase):
def test_declared_and_device_present_is_true(self):
lm = _make_lookup(PRESENT)
self.assertEqual(_run(lm, ["web-svc-libretranslate", "libretranslate"]), [True])
def test_declared_without_a_device_is_false(self):
"""The case that selected a -cuda tag with no manifest for the platform."""
lm = _make_lookup(ABSENT)
self.assertEqual(
_run(lm, ["web-svc-libretranslate", "libretranslate"]), [False]
)
def test_undeclared_with_a_device_is_false(self):
lm = _make_lookup(PRESENT)
self.assertEqual(_run(lm, ["web-app-plain", "plain"]), [False])
def test_missing_register_is_false(self):
"""Before sys-svc-container runs the register is absent, not falsy."""
lm = _make_lookup({})
self.assertEqual(
_run(lm, ["web-svc-libretranslate", "libretranslate"]), [False]
)
def test_a_device_without_its_driver_is_false(self):
"""A privileged container sees the host's /dev/nvidia0 without the driver."""
lm = _make_lookup(DRIVERLESS)
self.assertEqual(
_run(lm, ["web-svc-libretranslate", "libretranslate"]), [False]
)
def test_non_mapping_register_is_false(self):
lm = _make_lookup({"sys_svc_container_nvidia_device": "skipped"})
self.assertEqual(
_run(lm, ["web-svc-libretranslate", "libretranslate"]), [False]
)
def test_a_false_string_declaration_is_false(self):
"""Jinja's `| bool` reads "false" as False where bool() reads it as True.
The call sites this lookup replaces used `| bool`, so answering True
here would attach a GPU runtime nothing asked for.
"""
lm = _make_lookup(PRESENT)
self.assertEqual(_run(lm, ["web-app-stringy", "stringy"]), [False])
def test_a_yes_string_declaration_is_true(self):
lm = _make_lookup(PRESENT)
self.assertEqual(_run(lm, ["web-app-truthy", "truthy"]), [True])
def test_rejects_no_terms(self):
lm = _make_lookup(PRESENT)
with self.assertRaises(AnsibleError):
_run(lm, [])
def test_rejects_three_terms(self):
lm = _make_lookup(PRESENT)
with self.assertRaises(AnsibleError):
_run(lm, ["a", "b", "c"])
if __name__ == "__main__":
unittest.main()