feat: switch conftest + golden tests to batch-first resource loading
conftest: _resources_from_dump() prefers /device/0 batch reps; falls
back to flat resources dict. Fixes _parse_device0_batch to include
stub reps (rep={href:...}) as {} so capabilities still bind on
resources with no current data (e.g. /alarms/vs/0 when no alarm active).
test_golden_regression: _new_state_and_uids now uses by_type.for_device
to select per-device-type capabilities + pattern caps.
This commit is contained in:
@@ -49,8 +49,12 @@ def _parse_device0_batch(device0: list) -> dict[str, dict]:
|
||||
continue
|
||||
href = entry.get('href')
|
||||
rep = entry.get('rep')
|
||||
if href and isinstance(rep, dict) and set(rep.keys()) != {'href'}:
|
||||
out[href] = rep
|
||||
if not href:
|
||||
continue
|
||||
# rep == {"href": "..."} is a stub (resource present, no current data).
|
||||
# Include it as {} so capabilities still bind and the entity exists.
|
||||
if isinstance(rep, dict):
|
||||
out[href] = {} if set(rep.keys()) == {'href'} else rep
|
||||
return out
|
||||
|
||||
|
||||
|
||||
+12
-2
@@ -6,10 +6,20 @@ import pytest
|
||||
DUMPS = Path(__file__).resolve().parent.parent / 'local-tools' / 'dumps'
|
||||
|
||||
|
||||
def _resources_from_dump(dump: dict) -> dict[str, dict]:
|
||||
from samsung_appliance.bridge import _parse_device0_batch
|
||||
d0 = dump.get('device0')
|
||||
if isinstance(d0, list) and len(d0) > 1:
|
||||
batch = _parse_device0_batch(d0)
|
||||
if batch:
|
||||
return batch
|
||||
return {k: v for k, v in dump.get('resources', {}).items()
|
||||
if isinstance(v, dict)}
|
||||
|
||||
|
||||
def _load_resources(ip: str) -> dict[str, dict]:
|
||||
data = json.loads((DUMPS / f'{ip}.json').read_text())
|
||||
# Filter to only dict values (skip non-dict representations like "<timeout>")
|
||||
return {k: v for k, v in data['resources'].items() if isinstance(v, dict)}
|
||||
return _resources_from_dump(data)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
+1
-1
@@ -54,4 +54,4 @@
|
||||
"samsung_dishwasher_stop",
|
||||
"samsung_dishwasher_water_liters"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -59,4 +59,4 @@
|
||||
"samsung_refrigerator_rapid_fridge",
|
||||
"samsung_refrigerator_sabbath_mode"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+10
-6
@@ -1,11 +1,15 @@
|
||||
def test_dishwasher_resources_carry_rt(dishwasher_resources):
|
||||
def test_dishwasher_resources_loaded(dishwasher_resources):
|
||||
assert dishwasher_resources, "no resources loaded"
|
||||
# Every rep must expose its resource type so discovery can key on it.
|
||||
missing = [h for h, rep in dishwasher_resources.items()
|
||||
if not rep.get('rt')]
|
||||
assert not missing, f"resources without rt: {missing}"
|
||||
# Resources are sourced from the /device/0 batch; rt is not present in
|
||||
# batch responses (OCF carries rt at the collection-entry level only).
|
||||
# Verify the fixture contains dict values (including empty reps which are
|
||||
# valid batch entries for resources that temporarily have no state).
|
||||
non_dicts = [h for h, rep in dishwasher_resources.items()
|
||||
if not isinstance(rep, dict)]
|
||||
assert not non_dicts, f"resources with non-dict rep: {non_dicts}"
|
||||
|
||||
|
||||
def test_fridge_has_autofill(fridge_resources):
|
||||
assert '/autofill/vs/0' in fridge_resources
|
||||
assert 'x.com.samsung.da.autofill' in fridge_resources['/autofill/vs/0']['rt']
|
||||
# Batch reps do not carry 'rt'; check for the actual autofill field.
|
||||
assert 'x.com.samsung.da.autofill' in fridge_resources['/autofill/vs/0']
|
||||
|
||||
@@ -7,10 +7,18 @@ GOLDEN = Path(__file__).parent / 'fixtures' / 'golden'
|
||||
|
||||
|
||||
def _new_state_and_uids(name, resources):
|
||||
from samsung_appliance.registry.registry import CAPABILITIES
|
||||
from samsung_appliance.registry.by_type import for_device
|
||||
from samsung_appliance.registry.discovery import discover
|
||||
from samsung_appliance.registry.adapter import build_runtime_descriptor
|
||||
bound = discover(resources, CAPABILITIES)
|
||||
otn = resources.get('/otninformation/vs/0', {})
|
||||
one_ui = otn.get('swVersionInfo', {}).get('oneUiVersion', '')
|
||||
reg = for_device(one_ui) if one_ui else None
|
||||
if reg is None:
|
||||
from samsung_appliance.registry.registry import CAPABILITIES
|
||||
caps, pats = CAPABILITIES, []
|
||||
else:
|
||||
caps, pats = reg.capabilities, reg.pattern_capabilities
|
||||
bound = discover(resources, caps, pats)
|
||||
rd = build_runtime_descriptor(
|
||||
bound, topic_prefix=f'samsung_{name}', ha_prefix='homeassistant',
|
||||
device_name=name.title(), model='M', name=name, default_port=49154)
|
||||
@@ -24,7 +32,8 @@ def _new_state_and_uids(name, resources):
|
||||
('refrigerator', '10.0.0.254'),
|
||||
])
|
||||
def test_registry_reproduces_golden_state_keys(name, ip, request):
|
||||
resources = json.load(open(f'local-tools/dumps/{ip}.json'))['resources']
|
||||
from tests.conftest import _load_resources
|
||||
resources = _load_resources(ip)
|
||||
golden = json.loads((GOLDEN / f'{name}.json').read_text())
|
||||
state_keys, uids = _new_state_and_uids(name, resources)
|
||||
assert set(state_keys) == set(golden['state_keys']), (
|
||||
@@ -37,3 +46,16 @@ def test_registry_reproduces_golden_state_keys(name, ip, request):
|
||||
f" extra: {sorted(set(uids) - set(golden['discovery_unique_ids']))}\n"
|
||||
f" missing: {sorted(set(golden['discovery_unique_ids']) - set(uids))}"
|
||||
)
|
||||
|
||||
|
||||
def test_resources_from_batch_preferred_over_flat():
|
||||
from tests.conftest import _resources_from_dump
|
||||
dump = {
|
||||
'device0': [
|
||||
{'di': 'device'}, # [0] device-level rep, skipped
|
||||
{'href': '/foo', 'rep': {'x': 1}},
|
||||
],
|
||||
'resources': {'/foo': {'x': 99}},
|
||||
}
|
||||
result = _resources_from_dump(dump)
|
||||
assert result == {'/foo': {'x': 1}}
|
||||
|
||||
Reference in New Issue
Block a user