config: generalize config_validate into a shared schema engine

require_schema(config, schema) is a generic recursive declarative
checker (nested-dict schema, leaves name required type(s), each
intermediate object node gets its own dict-type check so a wrong-typed
section is named directly instead of surfacing as a confusing "missing
child" error). validate_temp_ctrl() is now just require_schema(config,
TEMPCTRL_SCHEMA) against a declarative schema dict, replacing the
hand-rolled nested loops.

utils/sud_validate.py's static structural check ('steps' must be a
list) now goes through the same require_schema()/SUD_SCHEMA - one
shared mechanism for both server config.json and sud.json documents.
The genuinely dynamic per-step check (ramp.rate required only when a
step sets its own temperature) stays hand-written, since it depends on
the document's content, not just its shape.

Added TestRequireSchema, exercising the generic engine directly
against a schema unrelated to TempCtrl.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F4oEE99rkKVU8L8tkzXWdG
This commit is contained in:
2026-07-11 20:40:32 +02:00
co-authored by Claude Sonnet 5
parent 4d59e11c1e
commit 92b9bdc1ba
3 changed files with 94 additions and 14 deletions
+31 -1
View File
@@ -1,6 +1,6 @@
import unittest
from utils.config_validate import ConfigError, require, validate_temp_ctrl
from utils.config_validate import ConfigError, require, require_schema, validate_temp_ctrl
def _valid_temp_ctrl_config():
@@ -42,6 +42,36 @@ class TestRequire(unittest.TestCase):
self.assertEqual(require({"a": 1.5}, "a", type=(int, float)), 1.5)
class TestRequireSchema(unittest.TestCase):
"""require_schema() is the generic engine both validate_temp_ctrl()
(config.json's TempCtrl section) and utils/sud_validate.py's
validate_sud() (a sud.json's fixed 'steps' shape) are built on - tested
here directly, against a schema unrelated to either, to confirm it's
not accidentally coupled to TempCtrl's specific shape."""
SCHEMA = {'a': {'b': int, 'c': (int, float)}, 'd': str}
def test_valid_document_passes(self):
require_schema({'a': {'b': 1, 'c': 2.5}, 'd': 'x'}, self.SCHEMA)
def test_missing_nested_leaf_raises(self):
with self.assertRaisesRegex(ConfigError, r'a\.b'):
require_schema({'a': {'c': 2.5}, 'd': 'x'}, self.SCHEMA)
def test_missing_top_level_leaf_raises(self):
with self.assertRaisesRegex(ConfigError, r'missing required config key: d$'):
require_schema({'a': {'b': 1, 'c': 2.5}}, self.SCHEMA)
def test_wrong_type_on_nested_object_itself_is_named_directly(self):
# 'a' present but not an object - must be reported as 'a' itself,
# not as a confusing "missing a.b"/"missing a.c".
with self.assertRaisesRegex(ConfigError, r'^config key a must be dict'):
require_schema({'a': 'not an object', 'd': 'x'}, self.SCHEMA)
def test_extra_keys_not_in_schema_are_ignored(self):
require_schema({'a': {'b': 1, 'c': 2.5}, 'd': 'x', 'extra': 'stray'}, self.SCHEMA)
class TestValidateTempCtrl(unittest.TestCase):
def test_valid_config_passes(self):
validate_temp_ctrl(_valid_temp_ctrl_config())