mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-07-24 09:36:31 +00:00
commit
d392b35fdd
22
.github/workflows/ci.yaml
vendored
22
.github/workflows/ci.yaml
vendored
@ -26,7 +26,7 @@ jobs:
|
||||
uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v2.1.1
|
||||
uses: actions/setup-python@v2.1.2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Restore Python virtual environment
|
||||
@ -69,7 +69,7 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v2.1.1
|
||||
uses: actions/setup-python@v2.1.2
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
@ -113,7 +113,7 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v2.1.1
|
||||
uses: actions/setup-python@v2.1.2
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
@ -157,7 +157,7 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v2.1.1
|
||||
uses: actions/setup-python@v2.1.2
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
@ -189,7 +189,7 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v2.1.1
|
||||
uses: actions/setup-python@v2.1.2
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
@ -230,7 +230,7 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v2.1.1
|
||||
uses: actions/setup-python@v2.1.2
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
@ -274,7 +274,7 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v2.1.1
|
||||
uses: actions/setup-python@v2.1.2
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
@ -306,7 +306,7 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v2.1.1
|
||||
uses: actions/setup-python@v2.1.2
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
@ -350,7 +350,7 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v2.1.1
|
||||
uses: actions/setup-python@v2.1.2
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
@ -391,7 +391,7 @@ jobs:
|
||||
-o console_output_style=count \
|
||||
tests
|
||||
- name: Upload coverage artifact
|
||||
uses: actions/upload-artifact@v2.1.3
|
||||
uses: actions/upload-artifact@v2.1.4
|
||||
with:
|
||||
name: coverage-${{ matrix.python-version }}
|
||||
path: .coverage
|
||||
@ -404,7 +404,7 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v2.1.1
|
||||
uses: actions/setup-python@v2.1.2
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
|
3
API.md
3
API.md
@ -45,6 +45,8 @@ Shows the installed add-ons from `addons`.
|
||||
"arch": "armhf|aarch64|i386|amd64",
|
||||
"channel": "stable|beta|dev",
|
||||
"timezone": "TIMEZONE",
|
||||
"healthy": "bool",
|
||||
"supported": "bool",
|
||||
"logging": "debug|info|warning|error|critical",
|
||||
"ip_address": "ip address",
|
||||
"wait_boot": "int",
|
||||
@ -785,6 +787,7 @@ return:
|
||||
"machine": "type",
|
||||
"arch": "arch",
|
||||
"supported_arch": ["arch1", "arch2"],
|
||||
"supported": "bool",
|
||||
"channel": "stable|beta|dev",
|
||||
"logging": "debug|info|warning|error|critical",
|
||||
"timezone": "Europe/Zurich"
|
||||
|
@ -1 +1 @@
|
||||
Subproject commit dec1f99a5fbfdafb348d30fa0f7a03f84b0d5d9e
|
||||
Subproject commit 77b25f5132820c0596ccae82dd501ce67f101e72
|
@ -5,8 +5,8 @@ cchardet==2.1.6
|
||||
colorlog==4.2.1
|
||||
cpe==1.2.1
|
||||
cryptography==3.0
|
||||
debugpy==1.0.0b12
|
||||
docker==4.2.2
|
||||
debugpy==1.0.0rc1
|
||||
docker==4.3.0
|
||||
gitpython==3.1.7
|
||||
jinja2==2.11.2
|
||||
packaging==20.4
|
||||
|
@ -98,6 +98,7 @@ class AddonManager(CoreSysAttributes):
|
||||
await addon.start()
|
||||
except Exception as err: # pylint: disable=broad-except
|
||||
_LOGGER.warning("Can't start Add-on %s: %s", addon.slug, err)
|
||||
self.sys_capture_exception(err)
|
||||
|
||||
await asyncio.sleep(self.sys_config.wait_boot)
|
||||
|
||||
@ -121,6 +122,7 @@ class AddonManager(CoreSysAttributes):
|
||||
await addon.stop()
|
||||
except Exception as err: # pylint: disable=broad-except
|
||||
_LOGGER.warning("Can't stop Add-on %s: %s", addon.slug, err)
|
||||
self.sys_capture_exception(err)
|
||||
|
||||
async def install(self, slug: str) -> None:
|
||||
"""Install an add-on."""
|
||||
|
@ -424,6 +424,11 @@ def _nested_validate_list(coresys, typ, data_list, key):
|
||||
"""Validate nested items."""
|
||||
options = []
|
||||
|
||||
# Make sure it is a list
|
||||
if not isinstance(data_list, list):
|
||||
raise vol.Invalid(f"Invalid list for {key}")
|
||||
|
||||
# Process list
|
||||
for element in data_list:
|
||||
# Nested?
|
||||
if isinstance(typ, dict):
|
||||
@ -439,6 +444,11 @@ def _nested_validate_dict(coresys, typ, data_dict, key):
|
||||
"""Validate nested items."""
|
||||
options = {}
|
||||
|
||||
# Make sure it is a dict
|
||||
if not isinstance(data_dict, dict):
|
||||
raise vol.Invalid(f"Invalid dict for {key}")
|
||||
|
||||
# Process dict
|
||||
for c_key, c_value in data_dict.items():
|
||||
# Ignore unknown options / remove from list
|
||||
if c_key not in typ:
|
||||
|
@ -14,6 +14,7 @@ from ..const import (
|
||||
ATTR_LOGGING,
|
||||
ATTR_MACHINE,
|
||||
ATTR_SUPERVISOR,
|
||||
ATTR_SUPPORTED,
|
||||
ATTR_SUPPORTED_ARCH,
|
||||
ATTR_TIMEZONE,
|
||||
)
|
||||
@ -38,6 +39,7 @@ class APIInfo(CoreSysAttributes):
|
||||
ATTR_MACHINE: self.sys_machine,
|
||||
ATTR_ARCH: self.sys_arch.default,
|
||||
ATTR_SUPPORTED_ARCH: self.sys_arch.supported,
|
||||
ATTR_SUPPORTED: self.sys_supported,
|
||||
ATTR_CHANNEL: self.sys_updater.channel,
|
||||
ATTR_LOGGING: self.sys_config.logging,
|
||||
ATTR_TIMEZONE: self.sys_timezone,
|
||||
|
@ -191,7 +191,11 @@ class APIIngress(CoreSysAttributes):
|
||||
async for data in result.content.iter_chunked(4096):
|
||||
await response.write(data)
|
||||
|
||||
except (aiohttp.ClientError, aiohttp.ClientPayloadError) as err:
|
||||
except (
|
||||
aiohttp.ClientError,
|
||||
aiohttp.ClientPayloadError,
|
||||
ConnectionResetError,
|
||||
) as err:
|
||||
_LOGGER.error("Stream error with %s: %s", url, err)
|
||||
|
||||
return response
|
||||
|
@ -1,9 +1,9 @@
|
||||
|
||||
try {
|
||||
new Function("import('/api/hassio/app/frontend_latest/entrypoint.a14fe829.js')")();
|
||||
new Function("import('/api/hassio/app/frontend_latest/entrypoint.855567b9.js')")();
|
||||
} catch (err) {
|
||||
var el = document.createElement('script');
|
||||
el.src = '/api/hassio/app/frontend_es5/entrypoint.24698450.js';
|
||||
el.src = '/api/hassio/app/frontend_es5/entrypoint.19035830.js';
|
||||
document.body.appendChild(el);
|
||||
}
|
||||
|
Binary file not shown.
File diff suppressed because one or more lines are too long
BIN
supervisor/api/panel/frontend_es5/entrypoint.19035830.js.gz
Normal file
BIN
supervisor/api/panel/frontend_es5/entrypoint.19035830.js.gz
Normal file
Binary file not shown.
@ -0,0 +1 @@
|
||||
{"version":3,"file":"entrypoint.19035830.js","sources":["webpack:///entrypoint.19035830.js"],"mappings":";AAAA","sourceRoot":""}
|
Binary file not shown.
@ -1 +0,0 @@
|
||||
{"version":3,"file":"entrypoint.24698450.js","sources":["webpack:///entrypoint.24698450.js"],"mappings":";AAAA","sourceRoot":""}
|
@ -1,3 +1,3 @@
|
||||
{
|
||||
"entrypoint.js": "/api/hassio/app/frontend_es5/entrypoint.24698450.js"
|
||||
"entrypoint.js": "/api/hassio/app/frontend_es5/entrypoint.19035830.js"
|
||||
}
|
File diff suppressed because one or more lines are too long
BIN
supervisor/api/panel/frontend_latest/entrypoint.855567b9.js.gz
Normal file
BIN
supervisor/api/panel/frontend_latest/entrypoint.855567b9.js.gz
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
@ -1,3 +1,3 @@
|
||||
{
|
||||
"entrypoint.js": "/api/hassio/app/frontend_latest/entrypoint.a14fe829.js"
|
||||
"entrypoint.js": "/api/hassio/app/frontend_latest/entrypoint.855567b9.js"
|
||||
}
|
@ -18,6 +18,7 @@ from ..const import (
|
||||
ATTR_DEBUG_BLOCK,
|
||||
ATTR_DESCRIPTON,
|
||||
ATTR_DIAGNOSTICS,
|
||||
ATTR_HEALTHY,
|
||||
ATTR_ICON,
|
||||
ATTR_INSTALLED,
|
||||
ATTR_IP_ADDRESS,
|
||||
@ -32,6 +33,7 @@ from ..const import (
|
||||
ATTR_REPOSITORY,
|
||||
ATTR_SLUG,
|
||||
ATTR_STATE,
|
||||
ATTR_SUPPORTED,
|
||||
ATTR_TIMEZONE,
|
||||
ATTR_VERSION,
|
||||
ATTR_VERSION_LATEST,
|
||||
@ -98,6 +100,8 @@ class APISupervisor(CoreSysAttributes):
|
||||
ATTR_VERSION_LATEST: self.sys_updater.version_supervisor,
|
||||
ATTR_CHANNEL: self.sys_updater.channel,
|
||||
ATTR_ARCH: self.sys_supervisor.arch,
|
||||
ATTR_SUPPORTED: self.sys_supported,
|
||||
ATTR_HEALTHY: self.sys_core.healthy,
|
||||
ATTR_IP_ADDRESS: str(self.sys_supervisor.ip_address),
|
||||
ATTR_WAIT_BOOT: self.sys_config.wait_boot,
|
||||
ATTR_TIMEZONE: self.sys_config.timezone,
|
||||
|
@ -280,10 +280,11 @@ def supervisor_debugger(coresys: CoreSys) -> None:
|
||||
|
||||
def setup_diagnostics(coresys: CoreSys) -> None:
|
||||
"""Sentry diagnostic backend."""
|
||||
_LOGGER.info("Initialize Supervisor Sentry")
|
||||
|
||||
def filter_data(event, hint):
|
||||
# Ignore issue if system is not supported or diagnostics is disabled
|
||||
if not coresys.config.diagnostics or not coresys.core.healthy:
|
||||
if not coresys.config.diagnostics or not coresys.supported:
|
||||
return None
|
||||
|
||||
# Not full startup - missing information
|
||||
@ -291,10 +292,9 @@ def setup_diagnostics(coresys: CoreSys) -> None:
|
||||
return event
|
||||
|
||||
# Update information
|
||||
with sentry_sdk.configure_scope() as scope:
|
||||
scope.set_context(
|
||||
"supervisor",
|
||||
{
|
||||
event.setdefault("extra", {}).update(
|
||||
{
|
||||
"supervisor": {
|
||||
"machine": coresys.machine,
|
||||
"arch": coresys.arch.default,
|
||||
"docker": coresys.docker.info.version,
|
||||
@ -308,23 +308,27 @@ def setup_diagnostics(coresys: CoreSys) -> None:
|
||||
"dns": coresys.plugins.dns.version,
|
||||
"multicast": coresys.plugins.multicast.version,
|
||||
"cli": coresys.plugins.cli.version,
|
||||
},
|
||||
)
|
||||
scope.set_tag(
|
||||
"installation_type",
|
||||
f"{'os' if coresys.hassos.available else 'supervised'}",
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
event.setdefault("tags", {}).update(
|
||||
{
|
||||
"installation_type": "os" if coresys.hassos.available else "supervised",
|
||||
"machine": coresys.machine,
|
||||
}
|
||||
)
|
||||
|
||||
return event
|
||||
|
||||
# Set log level
|
||||
sentry_logging = LoggingIntegration(
|
||||
level=logging.ERROR, event_level=logging.CRITICAL
|
||||
level=logging.WARNING, event_level=logging.CRITICAL
|
||||
)
|
||||
|
||||
sentry_sdk.init(
|
||||
dsn="https://9c6ea70f49234442b4746e447b24747e@o427061.ingest.sentry.io/5370612",
|
||||
before_send=filter_data,
|
||||
max_breadcrumbs=30,
|
||||
integrations=[AioHttpIntegration(), sentry_logging],
|
||||
release=SUPERVISOR_VERSION,
|
||||
)
|
||||
|
@ -3,8 +3,7 @@ from enum import Enum
|
||||
from ipaddress import ip_network
|
||||
from pathlib import Path
|
||||
|
||||
SUPERVISOR_VERSION = "231"
|
||||
|
||||
SUPERVISOR_VERSION = "232"
|
||||
|
||||
URL_HASSIO_ADDONS = "https://github.com/home-assistant/hassio-addons"
|
||||
URL_HASSIO_VERSION = "https://version.home-assistant.io/{channel}.json"
|
||||
@ -243,6 +242,8 @@ ATTR_ACTIVE = "active"
|
||||
ATTR_APPLICATION = "application"
|
||||
ATTR_INIT = "init"
|
||||
ATTR_DIAGNOSTICS = "diagnostics"
|
||||
ATTR_HEALTHY = "healthy"
|
||||
ATTR_SUPPORTED = "supported"
|
||||
|
||||
PROVIDE_SERVICE = "provide"
|
||||
NEED_SERVICE = "need"
|
||||
|
@ -19,45 +19,56 @@ class Core(CoreSysAttributes):
|
||||
"""Initialize Supervisor object."""
|
||||
self.coresys: CoreSys = coresys
|
||||
self.state: CoreStates = CoreStates.INITIALIZE
|
||||
self.healthy: bool = True
|
||||
self._healthy: bool = True
|
||||
|
||||
@property
|
||||
def healthy(self) -> bool:
|
||||
"""Return True if system is healthy."""
|
||||
return self._healthy and self.sys_supported
|
||||
|
||||
async def connect(self):
|
||||
"""Connect Supervisor container."""
|
||||
await self.sys_supervisor.load()
|
||||
|
||||
# If a update is failed?
|
||||
if self.sys_dev:
|
||||
self.sys_config.version = self.sys_supervisor.version
|
||||
elif self.sys_config.version != self.sys_supervisor.version:
|
||||
self.healthy = False
|
||||
_LOGGER.critical("Update of Supervisor fails!")
|
||||
|
||||
# If local docker is supported?
|
||||
# If host docker is supported?
|
||||
if not self.sys_docker.info.supported_version:
|
||||
self.healthy = False
|
||||
_LOGGER.critical(
|
||||
self.coresys.supported = False
|
||||
_LOGGER.error(
|
||||
"Docker version %s is not supported by Supervisor!",
|
||||
self.sys_docker.info.version,
|
||||
)
|
||||
elif self.sys_docker.info.inside_lxc:
|
||||
self.healthy = False
|
||||
_LOGGER.critical(
|
||||
self.coresys.supported = False
|
||||
_LOGGER.error(
|
||||
"Detected Docker running inside LXC. Running Home Assistant with the Supervisor on LXC is not supported!"
|
||||
)
|
||||
|
||||
self.sys_docker.info.check_requirements()
|
||||
|
||||
# Dbus available
|
||||
if not SOCKET_DBUS.exists():
|
||||
self.healthy = False
|
||||
_LOGGER.critical(
|
||||
self.coresys.supported = False
|
||||
_LOGGER.error(
|
||||
"DBus is required for Home Assistant. This system is not supported!"
|
||||
)
|
||||
|
||||
# Check if system is healthy
|
||||
if not self.healthy:
|
||||
_LOGGER.critical(
|
||||
"System running in a unhealthy state. Please update you OS or software!"
|
||||
# Check supervisor version/update
|
||||
if self.sys_dev:
|
||||
self.sys_config.version = self.sys_supervisor.version
|
||||
elif (
|
||||
self.sys_config.version == "dev"
|
||||
or self.sys_supervisor.instance.version == "dev"
|
||||
):
|
||||
self.coresys.supported = False
|
||||
_LOGGER.warning(
|
||||
"Found a development supervisor outside dev channel (%s)",
|
||||
self.sys_updater.channel,
|
||||
)
|
||||
elif self.sys_config.version != self.sys_supervisor.version:
|
||||
self._healthy = False
|
||||
_LOGGER.error(
|
||||
"Update %s of Supervisor %s fails!",
|
||||
self.sys_config.version,
|
||||
self.sys_supervisor.version,
|
||||
)
|
||||
|
||||
async def setup(self):
|
||||
@ -114,6 +125,14 @@ class Core(CoreSysAttributes):
|
||||
self.state = CoreStates.STARTUP
|
||||
await self.sys_api.start()
|
||||
|
||||
# Check if system is healthy
|
||||
if not self.sys_supported:
|
||||
_LOGGER.critical("System running in a unsupported environment!")
|
||||
elif not self.healthy:
|
||||
_LOGGER.critical(
|
||||
"System running in a unhealthy state and need manual intervention!"
|
||||
)
|
||||
|
||||
# Mark booted partition as healthy
|
||||
if self.sys_hassos.available:
|
||||
await self.sys_hassos.mark_healthy()
|
||||
|
@ -44,10 +44,13 @@ class CoreSys:
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize coresys."""
|
||||
# Static attributes
|
||||
# Static attributes protected
|
||||
self._machine_id: Optional[str] = None
|
||||
self._machine: Optional[str] = None
|
||||
|
||||
# Static attributes
|
||||
self.supported: bool = True
|
||||
|
||||
# External objects
|
||||
self._loop: asyncio.BaseEventLoop = asyncio.get_running_loop()
|
||||
self._websession: aiohttp.ClientSession = aiohttp.ClientSession()
|
||||
@ -459,6 +462,11 @@ class CoreSysAttributes:
|
||||
"""Return True if we run dev mode."""
|
||||
return self.coresys.dev
|
||||
|
||||
@property
|
||||
def sys_supported(self) -> bool:
|
||||
"""Return True if the system is supported."""
|
||||
return self.coresys.supported
|
||||
|
||||
@property
|
||||
def sys_timezone(self) -> str:
|
||||
"""Return timezone."""
|
||||
|
65
tests/addons/test_options_schema.py
Normal file
65
tests/addons/test_options_schema.py
Normal file
@ -0,0 +1,65 @@
|
||||
"""Test add-ons schema to UI schema convertion."""
|
||||
|
||||
import pytest
|
||||
import voluptuous as vol
|
||||
|
||||
from supervisor.addons.validate import validate_options
|
||||
|
||||
|
||||
def test_simple_schema(coresys):
|
||||
"""Test with simple schema."""
|
||||
assert validate_options(
|
||||
coresys,
|
||||
{"name": "str", "password": "password", "fires": "bool", "alias": "str?"},
|
||||
)({"name": "Pascal", "password": "1234", "fires": True, "alias": "test"})
|
||||
|
||||
assert validate_options(
|
||||
coresys,
|
||||
{"name": "str", "password": "password", "fires": "bool", "alias": "str?"},
|
||||
)({"name": "Pascal", "password": "1234", "fires": True})
|
||||
|
||||
with pytest.raises(vol.error.Invalid):
|
||||
validate_options(
|
||||
coresys,
|
||||
{"name": "str", "password": "password", "fires": "bool", "alias": "str?"},
|
||||
)({"name": "Pascal", "password": "1234", "fires": "hah"})
|
||||
|
||||
with pytest.raises(vol.error.Invalid):
|
||||
validate_options(
|
||||
coresys,
|
||||
{"name": "str", "password": "password", "fires": "bool", "alias": "str?"},
|
||||
)({"name": "Pascal", "fires": True})
|
||||
|
||||
|
||||
def test_complex_schema_list(coresys):
|
||||
"""Test with complex list schema."""
|
||||
assert validate_options(
|
||||
coresys, {"name": "str", "password": "password", "extend": ["str"]},
|
||||
)({"name": "Pascal", "password": "1234", "extend": ["test", "blu"]})
|
||||
|
||||
with pytest.raises(vol.error.Invalid):
|
||||
validate_options(
|
||||
coresys, {"name": "str", "password": "password", "extend": ["str"]},
|
||||
)({"name": "Pascal", "password": "1234", "extend": ["test", 1]})
|
||||
|
||||
with pytest.raises(vol.error.Invalid):
|
||||
validate_options(
|
||||
coresys, {"name": "str", "password": "password", "extend": ["str"]},
|
||||
)({"name": "Pascal", "password": "1234", "extend": "test"})
|
||||
|
||||
|
||||
def test_complex_schema_dict(coresys):
|
||||
"""Test with complex dict schema."""
|
||||
assert validate_options(
|
||||
coresys, {"name": "str", "password": "password", "extend": {"test": "int"}},
|
||||
)({"name": "Pascal", "password": "1234", "extend": {"test": 1}})
|
||||
|
||||
with pytest.raises(vol.error.Invalid):
|
||||
validate_options(
|
||||
coresys, {"name": "str", "password": "password", "extend": {"test": "int"}},
|
||||
)({"name": "Pascal", "password": "1234", "extend": {"wrong": 1}})
|
||||
|
||||
with pytest.raises(vol.error.Invalid):
|
||||
validate_options(
|
||||
coresys, {"name": "str", "password": "password", "extend": ["str"]},
|
||||
)({"name": "Pascal", "password": "1234", "extend": "test"})
|
Loading…
x
Reference in New Issue
Block a user