Files
computer-playbook/tests/unit/python/plugins/lookup/test_gpu.py
T
kevinveenbirkenbachandClaude Opus 5 819d710aaa fix(gpu): decide GPU acceleration in one lookup instead of at every call site
Whether a service gets a GPU needs two facts to agree: the declaration in
applications under services.<service>.gpu, and sys-svc-container's stat of
/dev/nvidia0 on the target host. The conjunction was restated at each call
site, and web-svc-libretranslate restated only the first half: it appended the
-cuda image suffix from the declaration alone. That tag has no arm64 manifest,
so the build died with `no match for platform in manifest`, which 10 of the 79
failing jobs of run 37252453141 carry, surfacing through
sys-svc-compose/handlers/{swarm,compose}.yml.

lookup('gpu', application_id[, service]) is now the single place that answers
it. Three call sites collapse onto it: both libretranslate roles' *_GPU var
and the runtime line of sys-svc-container's resource template. A lookup runs
on the controller and cannot stat the target, so it reads the register out of
the play's variables; the applications view comes through lookup_loader
because test_no_cache_imports_in_lookups forbids a lookup the direct
utils.cache import.

The coercion is ansible's boolean(), not python's bool(). The call sites this
replaces read the flag through Jinja's `| bool`, which answers False for the
strings "false", "False", "0" and "no" where bool() answers True, and a
templated declaration renders to such a string. Measured over 13 values the
two disagree on four of them; boolean(strict=False) matches `| bool` on all
13. Two tests pin it, and with bool() restored exactly the "false" string test
fails.

Measured rather than argued: over the merged applications, 1688
(application, service) pairs resolve the predicate identically under both
coercions, 0 disagreements. The predicate turns on for 21 services, not 5,
because resource_filter falls back to services.<entity_name>.gpu and every
service of a role whose entity declares gpu inherits it.

The assert_gpu include in both roles dropped its two trailing conditions: they
re-checked the register that *_GPU now already contains.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-05 17:45:00 +02:00

102 lines
3.5 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}}}
ABSENT: ClassVar[dict] = {"sys_svc_container_nvidia_device": {"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_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()