From e780484d24c1919bd5416d2b415280053c19e33b Mon Sep 17 00:00:00 2001 From: Tanmoy Sarkar Date: Tue, 9 Jun 2026 23:03:43 +0530 Subject: [PATCH] test(companion): Add config validation tests Cover validate_companion_workers (None becomes empty, non-list and non-dict items rejected) and CompanionConfig.config_hash (stable for equal configs, changes when a field changes, callable target keyed by qualified name and hashed stably). Co-Authored-By: Claude Opus 4.8 --- docs/design/companion-process-manager.md | 2 +- tests/test_companion_config.py | 39 ++++++++++++++++++++++-- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/docs/design/companion-process-manager.md b/docs/design/companion-process-manager.md index a93ac216..2c3f2ed6 100644 --- a/docs/design/companion-process-manager.md +++ b/docs/design/companion-process-manager.md @@ -695,7 +695,7 @@ No per-companion logic in Arbiter. - [x] Close manager-only fds in companion child. - [x] Add parent-death cleanup. - [x] Add lifecycle logs. -- [ ] Add tests for config validation. +- [x] Add tests for config validation. - [ ] Add tests for state transitions. - [ ] Add tests for control commands. - [ ] Add tests for transactional reread. diff --git a/tests/test_companion_config.py b/tests/test_companion_config.py index 6ed2d979..97391983 100644 --- a/tests/test_companion_config.py +++ b/tests/test_companion_config.py @@ -4,8 +4,8 @@ import pytest -from gunicorn.config import Config -from gunicorn.companion.config import build_companion_configs +from gunicorn.config import Config, validate_companion_workers +from gunicorn.companion.config import CompanionConfig, build_companion_configs def make_config(workers, **overrides): @@ -45,3 +45,38 @@ def test_build_requires_name_and_target(): build_companion_configs(make_config([{"name": "rq"}])) with pytest.raises(ValueError): build_companion_configs(make_config([{"target": "pkg:run"}])) + + +def test_validate_companion_workers_accepts_none_and_list(): + assert validate_companion_workers(None) == [] + workers = [{"name": "rq", "target": "pkg:run"}] + assert validate_companion_workers(workers) == workers + + +def test_validate_companion_workers_rejects_non_list(): + with pytest.raises(TypeError): + validate_companion_workers("rq") + + +def test_validate_companion_workers_rejects_non_dict_item(): + with pytest.raises(TypeError): + validate_companion_workers(["rq"]) + + +def test_config_hash_stable_and_field_sensitive(): + base = CompanionConfig(name="rq", target="pkg:run") + same = CompanionConfig(name="rq", target="pkg:run") + changed = CompanionConfig(name="rq", target="pkg:run", stop_timeout=99) + assert base.config_hash == same.config_hash + assert base.config_hash != changed.config_hash + + +def test_config_hash_keys_callable_target_by_qualified_name(): + def run(): + pass + + keyed = CompanionConfig._target_key(run) + assert ":" in keyed and keyed.endswith("run") + # A callable target hashes stably across CompanionConfig instances. + assert (CompanionConfig(name="rq", target=run).config_hash + == CompanionConfig(name="rq", target=run).config_hash)