sud: fail fast on malformed sud.json documents, one test per fixture
Sud.load() now calls utils/sud_validate.py's validate_sud() before parsing: requires a non-empty 'steps' list, and for every resolved step that sets its own 'temperature', a numeric ramp.rate - the one genuinely unguarded crash site (ramp['rate'], no .get() fallback) hit in tasks/sud.py, components/sud_forecast.py and demo_sud.py the moment that step is reached, which for SudForecastEstimator can be as early as the Load itself. A refused load is now logged with the specific reason (previously silent). validate_sud() reuses Sud._parse_data() directly rather than reimplementing default-merging/step-building. tests/utils/test_sud_validate_fixtures.py covers 8 fixture docs plus all 6 real sude/*.json files; tests/components/sud/test_sud_load.py exercises the Sud.load() wiring itself (rejects, keeps prior schedule). Also reworked both this and test_config_fixtures.py to generate one dedicated test method per fixture file (dynamic setattr) instead of looping with subTest inside a single method - subTest iterations don't show up individually in `unittest -v`/IDE test runners, which made it look like far fewer fixtures were covered than actually are. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F4oEE99rkKVU8L8tkzXWdG
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import unittest
|
||||
|
||||
from utils.config_validate import ConfigError, validate_temp_ctrl
|
||||
@@ -27,20 +28,33 @@ def _load(name):
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def _test_name(filename):
|
||||
return 'test_' + re.sub(r'\W', '_', filename[:-len('.json')])
|
||||
|
||||
|
||||
class TestConfigFixtures(unittest.TestCase):
|
||||
def test_every_fixture_file_is_covered(self):
|
||||
on_disk = {f for f in os.listdir(FIXTURES_DIR) if f.endswith('.json')}
|
||||
self.assertEqual(on_disk, set(CASES), "fixtures/ and CASES have drifted apart")
|
||||
|
||||
def test_fixtures_match_expected_outcome(self):
|
||||
for name, expected_error in CASES.items():
|
||||
with self.subTest(fixture=name):
|
||||
config = _load(name)
|
||||
if expected_error is None:
|
||||
validate_temp_ctrl(config) # must not raise
|
||||
else:
|
||||
with self.assertRaisesRegex(ConfigError, expected_error):
|
||||
validate_temp_ctrl(config)
|
||||
|
||||
def _make_fixture_test(name, expected_error):
|
||||
def test(self):
|
||||
config = _load(name)
|
||||
if expected_error is None:
|
||||
validate_temp_ctrl(config) # must not raise
|
||||
else:
|
||||
with self.assertRaisesRegex(ConfigError, expected_error):
|
||||
validate_temp_ctrl(config)
|
||||
return test
|
||||
|
||||
|
||||
# One dedicated test method per fixture file - not a single method looping
|
||||
# over CASES with subTest - so `unittest discover -v` (and any IDE test
|
||||
# runner) lists and can re-run each fixture individually instead of folding
|
||||
# them all into one opaque "did any of these fail" result.
|
||||
for _name, _expected_error in CASES.items():
|
||||
setattr(TestConfigFixtures, _test_name(_name), _make_fixture_test(_name, _expected_error))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
Reference in New Issue
Block a user