mirror of
https://github.com/Matysh/houseplan-card
synced 2026-07-31 08:28:31 +00:00
v1.50.0: the v1.49.0 review (HP-1490-01..04) and the owner's zoom batch
Owner's batch (committed to dev earlier today, released here):
- devices count as content for the default zoom;
- the editor no longer shifts the plan — the stage measures its own top
instead of assuming 118px of header;
- zoom goes out to 0.4x, centred.
From the review:
- HP-1490-01: the square-canvas migration wrote two stores in sequence, and
the first write deleted the aspects the second needed — a crash between
them stranded the layout in the old coordinates with nothing able to
finish it. The intent {space: old aspect} is durable now: saved to the
layout store before anything moves, cleared by the same write that stores
the migrated layout, each half idempotent behind its own trigger. The
update event fires only after both halves are on disk. Proven at the exact
crash boundary by a harness test that fails the layout write once.
- HP-1490-02: check_quota and the file write were two executor jobs with
nothing between them, so N parallel uploads all measured the store before
any of them wrote. One job under a dedicated upload_lock now — narrower
than write_lock on purpose, a directory scan must not stall config saves.
A failed write reserves nothing.
- HP-1490-03: the content frame fed pan, zoom, clamp AND pointer maths, so
the editors were boxed into yesterday's drawing. Edit modes measure from
the full square; mode switches refit rather than carry a view clamped
against the wrong base.
- HP-1490-04: Save could outrun the proportions read and ship the previous
file's ratio. Picking a plan clears it immediately; Save awaits the
bounded read and stores 'unknown' over a lie.
- §5: package-lock version synced, duplicated comment removed.
New: smoke_audit_1490.mjs, migration crash-recovery pure + harness tests,
parallel-quota harness test. Inventory: 138 unit / 49 pure / 40 harness / 64
smokes.
This commit is contained in:
@@ -34,3 +34,73 @@ async def test_unload(hass: HomeAssistant) -> None:
|
||||
assert await hass.config_entries.async_unload(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert entry.state.value == "not_loaded"
|
||||
|
||||
|
||||
async def test_square_migration_finishes_after_a_crash_between_the_writes(
|
||||
hass: HomeAssistant, hass_storage, monkeypatch
|
||||
) -> None:
|
||||
"""HP-1490-01, end to end on the real stores.
|
||||
|
||||
The layout write is made to fail once, AFTER the config write succeeded —
|
||||
the exact boundary that used to strand the layout in the old coordinates
|
||||
forever, because the config write had already deleted the `aspect` fields
|
||||
the layout half needed. The durable intent must finish the job on the next
|
||||
setup.
|
||||
"""
|
||||
from custom_components.houseplan.store import HouseplanStore
|
||||
|
||||
hass_storage["houseplan.config"] = {
|
||||
"version": 1, "data": {
|
||||
"config": {"spaces": [{"id": "f1", "aspect": 2.0, "rooms": []}],
|
||||
"markers": [], "settings": {}},
|
||||
"rev": 3,
|
||||
},
|
||||
}
|
||||
hass_storage["houseplan.layout"] = {
|
||||
"version": 1, "data": {"layout": {"m": {"s": "f1", "x": 0.1, "y": 0.1}}, "rev": 7},
|
||||
}
|
||||
|
||||
real_save = HouseplanStore.async_save
|
||||
state = {"layout_saves": 0}
|
||||
|
||||
async def failing_save(self, data):
|
||||
if self.key == "houseplan.layout" and "geom_pending" not in data:
|
||||
state["layout_saves"] += 1
|
||||
if state["layout_saves"] == 1:
|
||||
raise OSError("disk full at the worst possible moment")
|
||||
await real_save(self, data)
|
||||
|
||||
monkeypatch.setattr(HouseplanStore, "async_save", failing_save)
|
||||
|
||||
entry = MockConfigEntry(domain=DOMAIN, title="House Plan", data={}, options={})
|
||||
entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# the crash boundary: config migrated, layout not, intent saved
|
||||
cfg = hass_storage["houseplan.config"]["data"]["config"]
|
||||
assert "aspect" not in cfg["spaces"][0], "the config half committed"
|
||||
lay = hass_storage["houseplan.layout"]["data"]
|
||||
assert lay["layout"]["m"]["y"] == 0.1, "the layout half did NOT commit"
|
||||
assert lay.get("geom_pending") == {"f1": 2.0}, "but the intent is durable"
|
||||
|
||||
# next start: the store write works again
|
||||
monkeypatch.setattr(HouseplanStore, "async_save", real_save)
|
||||
if entry.state.value == "loaded":
|
||||
await hass.config_entries.async_unload(entry.entry_id)
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
lay = hass_storage["houseplan.layout"]["data"]
|
||||
assert lay["layout"]["m"] == {"s": "f1", "x": 0.1, "y": 0.3}, (
|
||||
"the saved intent finished the layout half"
|
||||
)
|
||||
assert "geom_pending" not in lay, "and left with the layout write"
|
||||
cfg = hass_storage["houseplan.config"]["data"]["config"]
|
||||
assert cfg["spaces"][0]["view_box"] == [0.0, 0.0, 1.0, 1.0]
|
||||
|
||||
# a third start changes nothing — both triggers are gone
|
||||
before = repr(hass_storage["houseplan.layout"]) + repr(hass_storage["houseplan.config"])
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert repr(hass_storage["houseplan.layout"]) + repr(hass_storage["houseplan.config"]) == before
|
||||
|
||||
Reference in New Issue
Block a user