Files
computer-playbook/tests/unit/plugins/lookup/test_timeout.py
T
kevinveenbirkenbachandClaude Opus 4.8 96aa7784b6 feat(sys-svc-compose): scale all task timeouts via TIMEOUT_FACTOR lookup
Add a 'timeout' lookup plugin that returns round(base * TIMEOUT_FACTOR)
(group_vars default 1, so value-identical to the old literals). One knob
in group_vars stretches every wait in the stack for a slow uplink:
TIMEOUT_FACTOR: 2 doubles them all.

- plugins/lookup/timeout.py + unit tests (identity, factor scaling,
  string coercion, error paths).
- group_vars/all/17_resource.yml: TIMEOUT_FACTOR: 1.
- Convert all 34 literal 'timeout:' task keywords / module args across 25
  task+handler files to "{{ lookup('timeout', <base>) | int }}".
- tests/lint/repository/yaml/test_timeout_uses_lookup.py enforces the
  lookup in every task/handler timeout (opt-out: # nocheck: timeout-lookup).
- swarm.yml pre-deploy build handler: add the missing timeout (same
  network-build hang vector as compose.yml's Build compose).
- Drop 13 pre-existing narration comments in the touched files (comment
  lint enforces whole-file on any edited file).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 13:57:30 +02:00

119 lines
3.5 KiB
Python

"""Unit tests for the timeout lookup plugin.
Pins the scaling contract:
* factor unset -> identity (value-identical drop-in for the literal).
* TIMEOUT_FACTOR from play vars scales the base.
* an explicit ``factor=`` kwarg overrides the global.
* numeric strings are coerced; non-numeric / negative base or factor raises.
* exactly one positional term is required.
"""
from __future__ import annotations
import importlib.util
import unittest
from ansible.errors import AnsibleError
from . import PROJECT_ROOT
def _load_lookup():
spec = importlib.util.spec_from_file_location(
"lookup_timeout", str(PROJECT_ROOT / "plugins/lookup/timeout.py")
)
mod = importlib.util.module_from_spec(spec)
assert spec and spec.loader
spec.loader.exec_module(mod)
return mod.LookupModule
class _DummyTemplar:
def __init__(self, available_variables=None):
self.available_variables = available_variables or {}
def template(self, value):
return value
class TestTimeoutLookup(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.LookupModule = _load_lookup()
def _make(self, variables):
lm = self.LookupModule()
lm._templar = _DummyTemplar(variables)
lm._loader = None
return lm
def test_factor_unset_is_identity(self):
lm = self._make({})
self.assertEqual(lm.run([3600], variables={}), [3600])
def test_factor_one_is_identity(self):
vars_ = {"TIMEOUT_FACTOR": 1}
lm = self._make(vars_)
self.assertEqual(lm.run([3600], variables=vars_), [3600])
def test_factor_two_doubles(self):
vars_ = {"TIMEOUT_FACTOR": 2}
lm = self._make(vars_)
self.assertEqual(lm.run([600], variables=vars_), [1200])
def test_fractional_factor_rounds(self):
vars_ = {"TIMEOUT_FACTOR": 1.5}
lm = self._make(vars_)
self.assertEqual(lm.run([100], variables=vars_), [150])
def test_string_base_coerced(self):
vars_ = {"TIMEOUT_FACTOR": 2}
lm = self._make(vars_)
self.assertEqual(lm.run(["300"], variables=vars_), [600])
def test_string_factor_coerced(self):
vars_ = {"TIMEOUT_FACTOR": "2"}
lm = self._make(vars_)
self.assertEqual(lm.run([300], variables=vars_), [600])
def test_explicit_factor_kwarg_overrides_global(self):
vars_ = {"TIMEOUT_FACTOR": 5}
lm = self._make(vars_)
self.assertEqual(lm.run([100], variables=vars_, factor=3), [300])
def test_zero_base_stays_zero(self):
vars_ = {"TIMEOUT_FACTOR": 4}
lm = self._make(vars_)
self.assertEqual(lm.run([0], variables=vars_), [0])
def test_no_term_raises(self):
lm = self._make({})
with self.assertRaises(AnsibleError):
lm.run([], variables={})
def test_too_many_terms_raises(self):
lm = self._make({})
with self.assertRaises(AnsibleError):
lm.run([1, 2], variables={})
def test_non_numeric_base_raises(self):
lm = self._make({})
with self.assertRaises(AnsibleError):
lm.run(["abc"], variables={})
def test_negative_factor_raises(self):
vars_ = {"TIMEOUT_FACTOR": -1}
lm = self._make(vars_)
with self.assertRaises(AnsibleError):
lm.run([100], variables=vars_)
def test_none_terms_raises(self):
lm = self._make({})
with self.assertRaises(AnsibleError):
lm.run(None, variables={})
if __name__ == "__main__":
unittest.main(verbosity=2)