Route device type from /oic/d as the primary detection signal
/oic/d's `rt` names the device's own OCF device type, which beats parsing board part numbers whenever a dump populates it. Add for_device_by_oic_type() and an _OIC_TYPE_TO_KEY table (airconditioner, dryer, refrigerator, washer, plus SmartThings' x.com.st.d.stickcleaner and x.com.st.d.steamcloset extensions), and consult it first in resolve(), ahead of modelNum/description and the resource-signature fallback. Thread the master's device_types from read_identity() through coordinator.py's discovery pass and config_flow's connection probe; subdevices keep resolving from their own /information/vs/0 (or the master's registry as a fallback) since they have no /oic/d of their own read today.
This commit is contained in:
7 files changed
+250
-35
No files matched your search
@@ -225,6 +225,7 @@ def _probe_and_validate(host: str, ca_cert_pem: str, ca_key_pem: str) -> dict:
|
||||
from smartthings_local.protocol.dtls_session import DtlsCoapSession
|
||||
from .registry.batch import parse_device0_batch
|
||||
from .registry.by_type import resolve as resolve_registry
|
||||
from .registry.identity import read_identity
|
||||
|
||||
_LOGGER.debug("Fetching Samsung cloud UUID from %s", _SAMSUNG_CLOUD_HOST)
|
||||
try:
|
||||
@@ -282,7 +283,15 @@ def _probe_and_validate(host: str, ca_cert_pem: str, ca_key_pem: str) -> dict:
|
||||
)
|
||||
if not serial or _is_placeholder_serial(serial):
|
||||
serial = f"{host}:{port}"
|
||||
recognized_registry = resolve_registry(resources)
|
||||
# /oic/d's device type (read_identity) is the primary detection
|
||||
# signal when a board populates it -- see registry/by_type's
|
||||
# resolve(). read_identity is defensive on every GET it makes, so
|
||||
# a device that doesn't answer /oic/p or /oic/d just yields an
|
||||
# empty device_types tuple here, falling through to the model-
|
||||
# string/resource-signature detection resolve() already did.
|
||||
identity = read_identity(sess, None)
|
||||
recognized_registry = resolve_registry(
|
||||
resources, device_types=identity.device_types)
|
||||
return {
|
||||
"port": port,
|
||||
"serial": serial,
|
||||
|
||||
@@ -634,6 +634,7 @@ class LocalThingsCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
bound, device_type_name, materialized, skipped = discover_partitioned(
|
||||
resources, self.subdevices, resolve_registry, CAPABILITIES,
|
||||
log=unbound.append, tier_log=_tier_log,
|
||||
oic_device_types=self._identity.device_types if self._identity else (),
|
||||
)
|
||||
self.subdevices = materialized
|
||||
self._skipped_subdevices = skipped
|
||||
@@ -667,12 +668,16 @@ class LocalThingsCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
if device_type_name is not None:
|
||||
self._log.debug("device type: %s (modelNum=%r)", device_type_name, model_num)
|
||||
else:
|
||||
# Both fields: detection reads each of them (board token, then
|
||||
# consumer-model code), and this line is what a user pastes into
|
||||
# an issue -- modelNum alone doesn't identify a washer or dryer.
|
||||
# All three: detection reads each of them (oic device type, then
|
||||
# board token, then consumer-model code), and this line is what a
|
||||
# user pastes into an issue -- modelNum alone doesn't identify a
|
||||
# washer or dryer, and device_types is often empty even when
|
||||
# populated hardware exists for a type we don't map yet.
|
||||
self._log.warning(
|
||||
"unknown device type modelNum=%r description=%r; using common caps",
|
||||
"unknown device type modelNum=%r description=%r device_types=%r; "
|
||||
"using common caps",
|
||||
model_num, description,
|
||||
self._identity.device_types if self._identity else (),
|
||||
)
|
||||
self.device_type_name = device_type_name
|
||||
self.bound = bound
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Per-device-type registries."""
|
||||
import re
|
||||
from typing import Optional
|
||||
from typing import Optional, Sequence
|
||||
|
||||
from ._base import DeviceRegistry
|
||||
from . import (
|
||||
@@ -10,7 +10,7 @@ from . import (
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
'DeviceRegistry', 'resolve', 'for_device_by_model',
|
||||
'DeviceRegistry', 'resolve', 'for_device_by_oic_type', 'for_device_by_model',
|
||||
'for_device_by_resources', '_board_tokens',
|
||||
]
|
||||
|
||||
@@ -189,6 +189,43 @@ def _consumer_model_key(description: str) -> Optional[str]:
|
||||
return None
|
||||
|
||||
|
||||
# /oic/d's `rt` (OCF's own device-type declaration, see registry/identity.py)
|
||||
# -> registry key. This is the device naming its own type -- no board-part
|
||||
# guessing involved -- so it's consulted before modelNum/description at all.
|
||||
#
|
||||
# Kept as narrow as the board-token table: every entry here is backed by a
|
||||
# real dump/issue, not a guess at what the OCF or SmartThings spec might
|
||||
# define. `x.com.st.d.*` entries are SmartThings' own vendor extension to
|
||||
# the OCF device-type vocabulary (used for categories with no `oic.d.*`
|
||||
# equivalent), same prefix convention as the `x.com.samsung.da.*` resource
|
||||
# fields elsewhere in this codebase.
|
||||
_OIC_TYPE_TO_KEY: dict[str, str] = {
|
||||
'oic.d.airconditioner': 'airconditioner',
|
||||
'oic.d.dryer': 'dryer',
|
||||
'oic.d.refrigerator': 'refrigerator',
|
||||
'oic.d.washer': 'washer',
|
||||
'x.com.st.d.stickcleaner': 'vacuum_station', # issue #219 -- stick-vacuum clean station
|
||||
'x.com.st.d.steamcloset': 'air_dresser', # AirDresser / steam-closet garment care
|
||||
}
|
||||
|
||||
|
||||
def for_device_by_oic_type(device_types: Sequence[str]) -> Optional[DeviceRegistry]:
|
||||
"""Device-type detection from /oic/d's `rt` -- OCF's own device-type
|
||||
declaration.
|
||||
|
||||
The primary path when a dump carries it: the device names its own type,
|
||||
so there's nothing to infer from board part numbers. Most hardware still
|
||||
doesn't populate `/oic/d` usefully -- see `resolve()`'s docstring -- so
|
||||
this only ever helps a minority of dumps, and `for_device_by_model`/
|
||||
`for_device_by_resources` remain load-bearing for everything else.
|
||||
"""
|
||||
for device_type in device_types:
|
||||
key = _OIC_TYPE_TO_KEY.get(device_type)
|
||||
if key is not None:
|
||||
return _REGISTRY_BY_KEY[key]
|
||||
return None
|
||||
|
||||
|
||||
def for_device_by_model(model_num: str, description: str) -> Optional[DeviceRegistry]:
|
||||
"""Device-type detection from /information/vs/0's model strings.
|
||||
|
||||
@@ -273,16 +310,22 @@ def for_device_by_resources(resources: dict[str, dict]) -> Optional[DeviceRegist
|
||||
return None
|
||||
|
||||
|
||||
def resolve(resources: dict[str, dict]) -> Optional[DeviceRegistry]:
|
||||
def resolve(
|
||||
resources: dict[str, dict], device_types: Sequence[str] = (),
|
||||
) -> Optional[DeviceRegistry]:
|
||||
"""Device type for a parsed /device/0 dump, or None if unrecognized.
|
||||
|
||||
The single entry point for detection -- the coordinator, the config
|
||||
flow's probe and the golden-regression harness all call this, so the
|
||||
order can't drift between what ships and what the tests assert.
|
||||
|
||||
Model strings first (`for_device_by_model`), then a distinctive resource
|
||||
signature (`for_device_by_resources`) for boards that report no
|
||||
/information/vs/0 at all.
|
||||
`device_types` (/oic/d's `rt`, read separately from the /device/0 dump --
|
||||
see registry/identity.py) is the primary signal when present: the device
|
||||
naming its own type beats parsing board part numbers. Falls back to model
|
||||
strings (`for_device_by_model`), then a distinctive resource signature
|
||||
(`for_device_by_resources`) for boards that report no /information/vs/0
|
||||
at all -- both unchanged from before /oic/d was ever consulted, since most
|
||||
hardware still doesn't populate it usefully.
|
||||
|
||||
`/otninformation/vs/0`'s oneUiVersion is deliberately not consulted. It
|
||||
reads like the obvious signal -- the device naming its own type, e.g.
|
||||
@@ -292,7 +335,11 @@ def resolve(resources: dict[str, dict]) -> Optional[DeviceRegistry]:
|
||||
is still reported in diagnostics as a firmware-generation marker.
|
||||
"""
|
||||
info = resources.get('/information/vs/0', {})
|
||||
return for_device_by_model(
|
||||
info.get('x.com.samsung.da.modelNum', ''),
|
||||
info.get('x.com.samsung.da.description', ''),
|
||||
) or for_device_by_resources(resources)
|
||||
return (
|
||||
for_device_by_oic_type(device_types)
|
||||
or for_device_by_model(
|
||||
info.get('x.com.samsung.da.modelNum', ''),
|
||||
info.get('x.com.samsung.da.description', ''),
|
||||
)
|
||||
or for_device_by_resources(resources)
|
||||
)
|
||||
@@ -47,13 +47,13 @@ def _device_types(d: dict) -> tuple[str, ...]:
|
||||
|
||||
In OCF this is the one standardized "what am I" field: alongside the
|
||||
generic 'oic.wk.d' it carries a concrete type such as 'oic.d.airconditioner'
|
||||
or a Samsung 'x.com.samsung.da.*' equivalent. Nothing routes on it yet --
|
||||
device-type detection currently parses board part numbers out of
|
||||
/information/vs/0's modelNum instead (see registry/by_type/__init__.py) --
|
||||
because no captured dump has ever included it: /device/0 batch responses
|
||||
don't carry /oic/d, and diagnostics didn't report it. It's surfaced in
|
||||
diagnostics so incoming issue reports can tell us whether real hardware
|
||||
populates it usefully enough to route on.
|
||||
or a SmartThings 'x.com.st.d.*' equivalent. `registry/by_type/resolve()`
|
||||
now consults this first, ahead of board-part-number parsing, via
|
||||
`for_device_by_oic_type` and its `_OIC_TYPE_TO_KEY` table -- but only a
|
||||
minority of dumps populate it, so the modelNum/description path stays
|
||||
load-bearing for everything else. It's also kept whole in diagnostics
|
||||
(see `raw` below) so incoming issue reports keep surfacing types that
|
||||
table doesn't know about yet.
|
||||
"""
|
||||
rt = d.get('rt')
|
||||
if isinstance(rt, str):
|
||||
|
||||
@@ -80,7 +80,7 @@ from __future__ import annotations
|
||||
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from typing import Callable, Optional
|
||||
from typing import Callable, Optional, Sequence
|
||||
|
||||
import cbor2
|
||||
|
||||
@@ -519,10 +519,11 @@ def _has_live_primary_entity(bound, state: dict) -> bool:
|
||||
def discover_partitioned(
|
||||
resources: dict[str, dict],
|
||||
subdevices: list['Subdevice'],
|
||||
resolve_registry: Callable[[dict], object],
|
||||
resolve_registry: Callable[..., object],
|
||||
fallback_capabilities: dict,
|
||||
log: Optional[Callable[[str], None]] = None,
|
||||
tier_log: Optional[Callable[[str, str], None]] = None,
|
||||
oic_device_types: Sequence[str] = (),
|
||||
):
|
||||
"""Bind every href in `resources` (the merged, real-href snapshot -- main
|
||||
plus every enumerated subdevice's seed) to entities, partitioned by which
|
||||
@@ -551,6 +552,15 @@ def discover_partitioned(
|
||||
first-discovery time only is a non-issue; getting a phantom subdevice
|
||||
silently counted into unbound_hrefs or hot/warm tiers is not.
|
||||
|
||||
`oic_device_types` (from the master's own `/oic/d`, see
|
||||
registry/identity.py) is passed only to the *master's* resolution --
|
||||
subdevices have no `/oic/d` of their own read today (they resolve from
|
||||
their own `/information/vs/0` or fall back to the master's whole
|
||||
registry, as documented above), and blindly applying the master's OCF
|
||||
device type to every subdevice's own model-based resolution would be
|
||||
wrong the moment a composite appliance ever pairs two genuinely
|
||||
different device types under one connection.
|
||||
|
||||
Returns `(bound, device_type_name, materialized, skipped)`:
|
||||
- `bound`: the concatenated BoundEntity list (main + every materialized
|
||||
subdevice).
|
||||
@@ -575,7 +585,7 @@ def discover_partitioned(
|
||||
# owned_elsewhere here too.
|
||||
main_view = canonical_view(MAIN, resources, subdevices)
|
||||
|
||||
reg = resolve_registry(main_view)
|
||||
reg = resolve_registry(main_view, device_types=oic_device_types)
|
||||
caps, pats = (
|
||||
(reg.capabilities, reg.pattern_capabilities) if reg is not None
|
||||
else (fallback_capabilities, [])
|
||||
|
||||
@@ -131,6 +131,53 @@ class TestConsumerModelKey:
|
||||
assert _consumer_model_key('ARTIK051_DONGLE_REF') is None
|
||||
|
||||
|
||||
class TestForDeviceByOicType:
|
||||
"""Primary device-type detection from /oic/d's `rt`."""
|
||||
|
||||
def test_every_oic_type_resolves_to_a_real_registry(self):
|
||||
from custom_components.localthings.registry.by_type import (
|
||||
_OIC_TYPE_TO_KEY, _REGISTRY_BY_KEY,
|
||||
)
|
||||
for oic_type, key in _OIC_TYPE_TO_KEY.items():
|
||||
assert key in _REGISTRY_BY_KEY, f"{oic_type!r} -> unknown registry {key!r}"
|
||||
|
||||
@pytest.mark.parametrize('oic_type, expected', [
|
||||
('oic.d.airconditioner', 'airconditioner'),
|
||||
('oic.d.dryer', 'dryer'),
|
||||
('oic.d.refrigerator', 'refrigerator'),
|
||||
('oic.d.washer', 'washer'),
|
||||
('x.com.st.d.stickcleaner', 'vacuum_station'),
|
||||
('x.com.st.d.steamcloset', 'air_dresser'),
|
||||
])
|
||||
def test_known_oic_types_resolve(self, oic_type, expected):
|
||||
from custom_components.localthings.registry.by_type import for_device_by_oic_type
|
||||
reg = for_device_by_oic_type((oic_type,))
|
||||
assert reg is not None
|
||||
assert reg.name == expected
|
||||
|
||||
def test_generic_wk_d_type_alone_resolves_nothing(self):
|
||||
"""The generic 'oic.wk.d' base type every OCF device carries
|
||||
alongside its concrete type isn't itself a device type."""
|
||||
from custom_components.localthings.registry.by_type import for_device_by_oic_type
|
||||
assert for_device_by_oic_type(('oic.wk.d',)) is None
|
||||
|
||||
def test_unrecognized_type_returns_none(self):
|
||||
from custom_components.localthings.registry.by_type import for_device_by_oic_type
|
||||
assert for_device_by_oic_type(('oic.d.somethingnew',)) is None
|
||||
|
||||
def test_empty_returns_none(self):
|
||||
from custom_components.localthings.registry.by_type import for_device_by_oic_type
|
||||
assert for_device_by_oic_type(()) is None
|
||||
|
||||
def test_finds_the_concrete_type_alongside_the_generic_one(self):
|
||||
"""A real /oic/d `rt` carries both the generic base type and the
|
||||
concrete one, order unspecified -- either position must resolve."""
|
||||
from custom_components.localthings.registry.by_type import for_device_by_oic_type
|
||||
reg = for_device_by_oic_type(('oic.wk.d', 'oic.d.washer'))
|
||||
assert reg is not None
|
||||
assert reg.name == 'washer'
|
||||
|
||||
|
||||
class TestForDeviceByModel:
|
||||
"""Fallback device-type detection for hardware without oneUiVersion."""
|
||||
|
||||
@@ -580,6 +627,57 @@ class TestOneUiVersionIsNotConsulted:
|
||||
|
||||
|
||||
class TestResolve:
|
||||
def test_oic_type_wins_over_model_strings(self):
|
||||
"""The device naming its own type via /oic/d beats board-token
|
||||
parsing -- an unrecognizable modelNum with a known oic.d type must
|
||||
still resolve, and a *conflicting* modelNum must lose to it."""
|
||||
from custom_components.localthings.registry.by_type import resolve
|
||||
resources = {
|
||||
'/information/vs/0': {
|
||||
'x.com.samsung.da.modelNum': 'SOME-UNKNOWN-BOARD',
|
||||
'x.com.samsung.da.description': 'SOME-UNKNOWN-BOARD',
|
||||
},
|
||||
}
|
||||
reg = resolve(resources, device_types=('oic.d.washer',))
|
||||
assert reg is not None
|
||||
assert reg.name == 'washer'
|
||||
|
||||
conflicting = resolve(
|
||||
resources={
|
||||
'/information/vs/0': {
|
||||
'x.com.samsung.da.modelNum': 'TP1X_REF_21K',
|
||||
'x.com.samsung.da.description': 'TP1X_REF_21K',
|
||||
},
|
||||
},
|
||||
device_types=('oic.d.washer',),
|
||||
)
|
||||
assert conflicting is not None
|
||||
assert conflicting.name == 'washer'
|
||||
|
||||
def test_empty_device_types_falls_back_to_model_strings(self):
|
||||
from custom_components.localthings.registry.by_type import resolve
|
||||
resources = {
|
||||
'/information/vs/0': {
|
||||
'x.com.samsung.da.modelNum': 'TP1X_REF_21K',
|
||||
'x.com.samsung.da.description': 'TP1X_REF_21K',
|
||||
},
|
||||
}
|
||||
reg = resolve(resources, device_types=())
|
||||
assert reg is not None
|
||||
assert reg.name == 'refrigerator'
|
||||
|
||||
def test_unmapped_device_types_falls_back_to_model_strings(self):
|
||||
from custom_components.localthings.registry.by_type import resolve
|
||||
resources = {
|
||||
'/information/vs/0': {
|
||||
'x.com.samsung.da.modelNum': 'TP1X_REF_21K',
|
||||
'x.com.samsung.da.description': 'TP1X_REF_21K',
|
||||
},
|
||||
}
|
||||
reg = resolve(resources, device_types=('oic.wk.d', 'oic.d.somethingnew'))
|
||||
assert reg is not None
|
||||
assert reg.name == 'refrigerator'
|
||||
|
||||
def test_prefers_model_strings_over_resource_signature(self, all_device_fixtures):
|
||||
"""Every fixture with usable model strings resolves the same way
|
||||
through `resolve` as through `for_device_by_model` directly."""
|
||||
|
||||
+56
-10
@@ -476,7 +476,7 @@ def test_discover_partitioned_binds_main_and_subdevice_separately():
|
||||
'/mode/vs/1': {'m': 'sibling'},
|
||||
}
|
||||
|
||||
def resolve(_resources):
|
||||
def resolve(_resources, **_kwargs):
|
||||
return reg
|
||||
|
||||
bound, device_type_name, materialized, skipped = discover_partitioned(
|
||||
@@ -516,7 +516,7 @@ def test_discover_partitioned_main_pass_excludes_subdevice_hrefs_from_unbound():
|
||||
|
||||
unbound = []
|
||||
discover_partitioned(
|
||||
resources, [sub1], lambda r: reg, fallback_capabilities={},
|
||||
resources, [sub1], lambda r, **_: reg, fallback_capabilities={},
|
||||
log=unbound.append,
|
||||
)
|
||||
assert unbound == []
|
||||
@@ -534,7 +534,7 @@ def test_discover_partitioned_subdevice_resolves_its_own_registry():
|
||||
sub1 = _indexed('1')
|
||||
resources = {'/mode/vs/0': {'x': 1}, '/mode/vs/1': {'x': 2}}
|
||||
|
||||
def resolve(view):
|
||||
def resolve(view, **_kwargs):
|
||||
# The subdevice's canonical view is exactly {'/mode/vs/0': {'x': 2}}.
|
||||
return sub_reg if view.get('/mode/vs/0', {}).get('x') == 2 else main_reg
|
||||
|
||||
@@ -563,7 +563,7 @@ def test_discover_partitioned_subdevice_falls_back_to_master_registry():
|
||||
'/mode/vs/1': {'x': 2},
|
||||
}
|
||||
|
||||
def resolve(view):
|
||||
def resolve(view, **_kwargs):
|
||||
return main_reg if view.get('/information/vs/0') else None
|
||||
|
||||
bound, _, materialized, skipped = discover_partitioned(
|
||||
@@ -575,6 +575,52 @@ def test_discover_partitioned_subdevice_falls_back_to_master_registry():
|
||||
assert '/mode/vs/1' in hrefs
|
||||
|
||||
|
||||
def test_discover_partitioned_oic_device_types_resolves_main_pass():
|
||||
"""`oic_device_types` reaches the main-pass resolve call as
|
||||
`device_types=`, letting a fake `resolve_registry` mirror the real
|
||||
resolve()'s precedence between /oic/d and model-based detection."""
|
||||
cap = Capability(href='/mode/vs/0', entities=(BinarySensorDesc(key='m', field='x'),))
|
||||
oic_reg = _FakeRegistry('washer', {'/mode/vs/0': [cap]})
|
||||
resources = {'/mode/vs/0': {'x': 1}}
|
||||
|
||||
def resolve(_resources, device_types=()):
|
||||
return oic_reg if 'oic.d.washer' in device_types else None
|
||||
|
||||
bound, device_type_name, _, _ = discover_partitioned(
|
||||
resources, [], resolve, fallback_capabilities={},
|
||||
oic_device_types=('oic.d.washer',),
|
||||
)
|
||||
assert device_type_name == 'washer'
|
||||
assert {b.href for b in bound} == {'/mode/vs/0'}
|
||||
|
||||
|
||||
def test_discover_partitioned_oic_device_types_not_applied_to_subdevices():
|
||||
"""The master's own /oic/d type must not leak into a subdevice's own
|
||||
resolution -- only main_view's resolve() call receives it, so a fake
|
||||
resolver keyed purely on device_types resolves nothing for the
|
||||
subdevice pass and it falls back to the master's registry, per the
|
||||
documented (and unchanged) subdevice-fallback behavior."""
|
||||
cap = Capability(href='/mode/vs/0', entities=(BinarySensorDesc(key='m', field='x'),))
|
||||
oic_reg = _FakeRegistry('washer', {'/mode/vs/0': [cap]})
|
||||
sub1 = _indexed('1')
|
||||
resources = {'/mode/vs/0': {'x': 1}, '/mode/vs/1': {'x': 2}}
|
||||
|
||||
def resolve(_resources, device_types=()):
|
||||
return oic_reg if 'oic.d.washer' in device_types else None
|
||||
|
||||
bound, device_type_name, materialized, skipped = discover_partitioned(
|
||||
resources, [sub1], resolve, fallback_capabilities={},
|
||||
oic_device_types=('oic.d.washer',),
|
||||
)
|
||||
assert device_type_name == 'washer'
|
||||
assert materialized == [sub1]
|
||||
assert skipped == []
|
||||
# The subdevice's own resolve() call sees no device_types and returns
|
||||
# None, falling back to the master's oic_reg -- same capabilities, so
|
||||
# /mode/vs/1 still binds via that shared registry.
|
||||
assert {b.href for b in bound} == {'/mode/vs/0', '/mode/vs/1'}
|
||||
|
||||
|
||||
def test_discover_partitioned_no_subdevices_matches_plain_discover():
|
||||
"""For a device with no subdevices this must be exactly the single
|
||||
discover() call it replaces -- the hard regression guard the whole
|
||||
@@ -586,7 +632,7 @@ def test_discover_partitioned_no_subdevices_matches_plain_discover():
|
||||
resources = {'/mode/vs/0': {'x': 1}}
|
||||
|
||||
bound_via_helper, _, materialized, skipped = discover_partitioned(
|
||||
resources, [], lambda r: reg, fallback_capabilities={},
|
||||
resources, [], lambda r, **_: reg, fallback_capabilities={},
|
||||
)
|
||||
bound_direct = discover(resources, reg.capabilities, reg.pattern_capabilities)
|
||||
|
||||
@@ -619,7 +665,7 @@ def test_discover_partitioned_skips_candidate_with_no_live_primary_entity():
|
||||
}
|
||||
|
||||
bound, _, materialized, skipped = discover_partitioned(
|
||||
resources, [unit2], lambda r: reg, fallback_capabilities={},
|
||||
resources, [unit2], lambda r, **_: reg, fallback_capabilities={},
|
||||
)
|
||||
assert materialized == []
|
||||
assert len(skipped) == 1
|
||||
@@ -654,7 +700,7 @@ def test_discover_partitioned_materializes_candidate_with_live_primary_entity():
|
||||
}
|
||||
|
||||
bound, _, materialized, skipped = discover_partitioned(
|
||||
resources, [sub1], lambda r: reg, fallback_capabilities={},
|
||||
resources, [sub1], lambda r, **_: reg, fallback_capabilities={},
|
||||
)
|
||||
assert materialized == [sub1]
|
||||
assert skipped == []
|
||||
@@ -692,7 +738,7 @@ def test_discover_partitioned_skips_candidate_whose_only_live_primary_is_a_meter
|
||||
}
|
||||
|
||||
bound, _, materialized, skipped = discover_partitioned(
|
||||
resources, [unit1], lambda r: reg, fallback_capabilities={},
|
||||
resources, [unit1], lambda r, **_: reg, fallback_capabilities={},
|
||||
)
|
||||
assert materialized == []
|
||||
assert [s.subdevice for s in skipped] == [unit1]
|
||||
@@ -726,7 +772,7 @@ def test_discover_partitioned_meter_carve_out_does_not_gate_out_a_live_subdevice
|
||||
}
|
||||
|
||||
bound, _, materialized, skipped = discover_partitioned(
|
||||
resources, [unit1], lambda r: reg, fallback_capabilities={},
|
||||
resources, [unit1], lambda r, **_: reg, fallback_capabilities={},
|
||||
)
|
||||
assert materialized == [unit1]
|
||||
assert skipped == []
|
||||
@@ -748,7 +794,7 @@ def test_discover_partitioned_skipped_candidate_contributes_no_hot_warm_hrefs():
|
||||
|
||||
tiers = []
|
||||
discover_partitioned(
|
||||
resources, [unit2], lambda r: reg, fallback_capabilities={},
|
||||
resources, [unit2], lambda r, **_: reg, fallback_capabilities={},
|
||||
tier_log=lambda href, tier: tiers.append(href),
|
||||
)
|
||||
assert '/mode/vs/2' not in tiers
|
||||
Reference in new issue
Block a user