Make the check_config script open issue_registry read only (#98545)
* Don't blow up if validators can't access the issue registry * Make the check_config script open issue_registry read only * Update tests/helpers/test_issue_registry.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> --------- Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
parent
90b9764578
commit
7fcc2dd44e
6 changed files with 115 additions and 4 deletions
|
@ -1122,6 +1122,7 @@ def _no_yaml_config_schema(
|
||||||
# pylint: disable-next=import-outside-toplevel
|
# pylint: disable-next=import-outside-toplevel
|
||||||
from .issue_registry import IssueSeverity, async_create_issue
|
from .issue_registry import IssueSeverity, async_create_issue
|
||||||
|
|
||||||
|
# HomeAssistantError is raised if called from the wrong thread
|
||||||
with contextlib.suppress(HomeAssistantError):
|
with contextlib.suppress(HomeAssistantError):
|
||||||
hass = async_get_hass()
|
hass = async_get_hass()
|
||||||
async_create_issue(
|
async_create_issue(
|
||||||
|
|
|
@ -95,16 +95,18 @@ class IssueRegistryStore(Store[dict[str, list[dict[str, Any]]]]):
|
||||||
class IssueRegistry:
|
class IssueRegistry:
|
||||||
"""Class to hold a registry of issues."""
|
"""Class to hold a registry of issues."""
|
||||||
|
|
||||||
def __init__(self, hass: HomeAssistant) -> None:
|
def __init__(self, hass: HomeAssistant, *, read_only: bool = False) -> None:
|
||||||
"""Initialize the issue registry."""
|
"""Initialize the issue registry."""
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
self.issues: dict[tuple[str, str], IssueEntry] = {}
|
self.issues: dict[tuple[str, str], IssueEntry] = {}
|
||||||
|
self._read_only = read_only
|
||||||
self._store = IssueRegistryStore(
|
self._store = IssueRegistryStore(
|
||||||
hass,
|
hass,
|
||||||
STORAGE_VERSION_MAJOR,
|
STORAGE_VERSION_MAJOR,
|
||||||
STORAGE_KEY,
|
STORAGE_KEY,
|
||||||
atomic_writes=True,
|
atomic_writes=True,
|
||||||
minor_version=STORAGE_VERSION_MINOR,
|
minor_version=STORAGE_VERSION_MINOR,
|
||||||
|
read_only=read_only,
|
||||||
)
|
)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
|
@ -278,10 +280,10 @@ def async_get(hass: HomeAssistant) -> IssueRegistry:
|
||||||
return cast(IssueRegistry, hass.data[DATA_REGISTRY])
|
return cast(IssueRegistry, hass.data[DATA_REGISTRY])
|
||||||
|
|
||||||
|
|
||||||
async def async_load(hass: HomeAssistant) -> None:
|
async def async_load(hass: HomeAssistant, *, read_only: bool = False) -> None:
|
||||||
"""Load issue registry."""
|
"""Load issue registry."""
|
||||||
assert DATA_REGISTRY not in hass.data
|
assert DATA_REGISTRY not in hass.data
|
||||||
hass.data[DATA_REGISTRY] = IssueRegistry(hass)
|
hass.data[DATA_REGISTRY] = IssueRegistry(hass, read_only=read_only)
|
||||||
await hass.data[DATA_REGISTRY].async_load()
|
await hass.data[DATA_REGISTRY].async_load()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -93,6 +93,7 @@ class Store(Generic[_T]):
|
||||||
atomic_writes: bool = False,
|
atomic_writes: bool = False,
|
||||||
encoder: type[JSONEncoder] | None = None,
|
encoder: type[JSONEncoder] | None = None,
|
||||||
minor_version: int = 1,
|
minor_version: int = 1,
|
||||||
|
read_only: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize storage class."""
|
"""Initialize storage class."""
|
||||||
self.version = version
|
self.version = version
|
||||||
|
@ -107,6 +108,7 @@ class Store(Generic[_T]):
|
||||||
self._load_task: asyncio.Future[_T | None] | None = None
|
self._load_task: asyncio.Future[_T | None] | None = None
|
||||||
self._encoder = encoder
|
self._encoder = encoder
|
||||||
self._atomic_writes = atomic_writes
|
self._atomic_writes = atomic_writes
|
||||||
|
self._read_only = read_only
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def path(self):
|
def path(self):
|
||||||
|
@ -344,6 +346,9 @@ class Store(Generic[_T]):
|
||||||
|
|
||||||
self._data = None
|
self._data = None
|
||||||
|
|
||||||
|
if self._read_only:
|
||||||
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await self._async_write_data(self.path, data)
|
await self._async_write_data(self.path, data)
|
||||||
except (json_util.SerializationError, WriteError) as err:
|
except (json_util.SerializationError, WriteError) as err:
|
||||||
|
|
|
@ -19,6 +19,7 @@ from homeassistant.helpers import (
|
||||||
area_registry as ar,
|
area_registry as ar,
|
||||||
device_registry as dr,
|
device_registry as dr,
|
||||||
entity_registry as er,
|
entity_registry as er,
|
||||||
|
issue_registry as ir,
|
||||||
)
|
)
|
||||||
from homeassistant.helpers.check_config import async_check_ha_config_file
|
from homeassistant.helpers.check_config import async_check_ha_config_file
|
||||||
from homeassistant.util.yaml import Secrets
|
from homeassistant.util.yaml import Secrets
|
||||||
|
@ -237,6 +238,7 @@ async def async_check_config(config_dir):
|
||||||
await ar.async_load(hass)
|
await ar.async_load(hass)
|
||||||
await dr.async_load(hass)
|
await dr.async_load(hass)
|
||||||
await er.async_load(hass)
|
await er.async_load(hass)
|
||||||
|
await ir.async_load(hass, read_only=True)
|
||||||
components = await async_check_ha_config_file(hass)
|
components = await async_check_ha_config_file(hass)
|
||||||
await hass.async_stop(force=True)
|
await hass.async_stop(force=True)
|
||||||
return components
|
return components
|
||||||
|
|
|
@ -9,7 +9,7 @@ from homeassistant.helpers import issue_registry as ir
|
||||||
from tests.common import async_capture_events, flush_store
|
from tests.common import async_capture_events, flush_store
|
||||||
|
|
||||||
|
|
||||||
async def test_load_issues(hass: HomeAssistant) -> None:
|
async def test_load_save_issues(hass: HomeAssistant) -> None:
|
||||||
"""Make sure that we can load/save data correctly."""
|
"""Make sure that we can load/save data correctly."""
|
||||||
issues = [
|
issues = [
|
||||||
{
|
{
|
||||||
|
@ -209,6 +209,77 @@ async def test_load_issues(hass: HomeAssistant) -> None:
|
||||||
assert issue4_registry2 == issue4
|
assert issue4_registry2 == issue4
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("load_registries", [False])
|
||||||
|
async def test_load_save_issues_read_only(
|
||||||
|
hass: HomeAssistant, hass_storage: dict[str, Any]
|
||||||
|
) -> None:
|
||||||
|
"""Make sure that we don't save data when opened in read-only mode."""
|
||||||
|
hass_storage[ir.STORAGE_KEY] = {
|
||||||
|
"version": ir.STORAGE_VERSION_MAJOR,
|
||||||
|
"minor_version": ir.STORAGE_VERSION_MINOR,
|
||||||
|
"data": {
|
||||||
|
"issues": [
|
||||||
|
{
|
||||||
|
"created": "2022-07-19T09:41:13.746514+00:00",
|
||||||
|
"dismissed_version": "2022.7.0.dev0",
|
||||||
|
"domain": "test",
|
||||||
|
"is_persistent": False,
|
||||||
|
"issue_id": "issue_1",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
issues = [
|
||||||
|
{
|
||||||
|
"breaks_in_ha_version": "2022.8",
|
||||||
|
"domain": "test",
|
||||||
|
"issue_id": "issue_2",
|
||||||
|
"is_fixable": True,
|
||||||
|
"is_persistent": False,
|
||||||
|
"learn_more_url": "https://theuselessweb.com/abc",
|
||||||
|
"severity": "other",
|
||||||
|
"translation_key": "even_worse",
|
||||||
|
"translation_placeholders": {"def": "456"},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
events = async_capture_events(hass, ir.EVENT_REPAIRS_ISSUE_REGISTRY_UPDATED)
|
||||||
|
await ir.async_load(hass, read_only=True)
|
||||||
|
|
||||||
|
for issue in issues:
|
||||||
|
ir.async_create_issue(
|
||||||
|
hass,
|
||||||
|
issue["domain"],
|
||||||
|
issue["issue_id"],
|
||||||
|
breaks_in_ha_version=issue["breaks_in_ha_version"],
|
||||||
|
is_fixable=issue["is_fixable"],
|
||||||
|
is_persistent=issue["is_persistent"],
|
||||||
|
learn_more_url=issue["learn_more_url"],
|
||||||
|
severity=issue["severity"],
|
||||||
|
translation_key=issue["translation_key"],
|
||||||
|
translation_placeholders=issue["translation_placeholders"],
|
||||||
|
)
|
||||||
|
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert len(events) == 1
|
||||||
|
assert events[0].data == {
|
||||||
|
"action": "create",
|
||||||
|
"domain": "test",
|
||||||
|
"issue_id": "issue_2",
|
||||||
|
}
|
||||||
|
|
||||||
|
registry = ir.async_get(hass)
|
||||||
|
assert len(registry.issues) == 2
|
||||||
|
|
||||||
|
registry2 = ir.IssueRegistry(hass)
|
||||||
|
await flush_store(registry._store)
|
||||||
|
await registry2.async_load()
|
||||||
|
|
||||||
|
assert len(registry2.issues) == 1
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("load_registries", [False])
|
@pytest.mark.parametrize("load_registries", [False])
|
||||||
async def test_loading_issues_from_storage(
|
async def test_loading_issues_from_storage(
|
||||||
hass: HomeAssistant, hass_storage: dict[str, Any]
|
hass: HomeAssistant, hass_storage: dict[str, Any]
|
||||||
|
|
|
@ -60,6 +60,12 @@ def store_v_2_1(hass):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def read_only_store(hass):
|
||||||
|
"""Fixture of a read only store."""
|
||||||
|
return storage.Store(hass, MOCK_VERSION, MOCK_KEY, read_only=True)
|
||||||
|
|
||||||
|
|
||||||
async def test_loading(hass: HomeAssistant, store) -> None:
|
async def test_loading(hass: HomeAssistant, store) -> None:
|
||||||
"""Test we can save and load data."""
|
"""Test we can save and load data."""
|
||||||
await store.async_save(MOCK_DATA)
|
await store.async_save(MOCK_DATA)
|
||||||
|
@ -703,3 +709,27 @@ async def test_os_error_is_fatal(tmpdir: py.path.local) -> None:
|
||||||
await store.async_load()
|
await store.async_load()
|
||||||
|
|
||||||
await hass.async_stop(force=True)
|
await hass.async_stop(force=True)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_read_only_store(
|
||||||
|
hass: HomeAssistant, read_only_store, hass_storage: dict[str, Any]
|
||||||
|
) -> None:
|
||||||
|
"""Test store opened in read only mode does not save."""
|
||||||
|
read_only_store.async_delay_save(lambda: MOCK_DATA, 1)
|
||||||
|
assert read_only_store.key not in hass_storage
|
||||||
|
|
||||||
|
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=1))
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert read_only_store.key not in hass_storage
|
||||||
|
|
||||||
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
|
||||||
|
hass.state = CoreState.stopping
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10))
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert read_only_store.key not in hass_storage
|
||||||
|
|
||||||
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_FINAL_WRITE)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert read_only_store.key not in hass_storage
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue