core/tests/components/command_line/test_cover.py

412 lines
12 KiB
Python
Raw Normal View History

"""The tests the cover command line platform."""
from __future__ import annotations
import asyncio
from datetime import timedelta
import os
import tempfile
2021-01-01 21:31:56 +00:00
from unittest.mock import patch
import pytest
from homeassistant import setup
from homeassistant.components.command_line import DOMAIN
from homeassistant.components.command_line.cover import CommandCover
from homeassistant.components.cover import DOMAIN as COVER_DOMAIN, SCAN_INTERVAL
from homeassistant.components.homeassistant import (
DOMAIN as HA_DOMAIN,
SERVICE_UPDATE_ENTITY,
)
from homeassistant.const import (
2019-07-31 19:25:30 +00:00
ATTR_ENTITY_ID,
SERVICE_CLOSE_COVER,
SERVICE_OPEN_COVER,
SERVICE_STOP_COVER,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
import homeassistant.helpers.issue_registry as ir
2021-03-01 16:27:04 +00:00
import homeassistant.util.dt as dt_util
from tests.common import async_fire_time_changed
async def test_no_covers_platform_yaml(
caplog: pytest.LogCaptureFixture, hass: HomeAssistant
) -> None:
2021-03-01 16:27:04 +00:00
"""Test that the cover does not polls when there's no state command."""
2021-03-01 16:27:04 +00:00
with patch(
"homeassistant.components.command_line.utils.subprocess.check_output",
2021-03-01 16:27:04 +00:00
return_value=b"50\n",
):
assert await setup.async_setup_component(
hass,
COVER_DOMAIN,
{
COVER_DOMAIN: [
{"platform": "command_line", "covers": {}},
]
},
)
await hass.async_block_till_done()
2021-03-01 16:27:04 +00:00
assert "No covers added" in caplog.text
issue_registry = ir.async_get(hass)
issue = issue_registry.async_get_issue(DOMAIN, "deprecated_yaml_cover")
assert issue.translation_key == "deprecated_platform_yaml"
async def test_state_value_platform_yaml(hass: HomeAssistant) -> None:
"""Test with state value."""
with tempfile.TemporaryDirectory() as tempdirname:
path = os.path.join(tempdirname, "cover_status")
assert await setup.async_setup_component(
hass,
COVER_DOMAIN,
{
COVER_DOMAIN: [
{
"platform": "command_line",
"covers": {
"test": {
"command_state": f"cat {path}",
"command_open": f"echo 1 > {path}",
"command_close": f"echo 1 > {path}",
"command_stop": f"echo 0 > {path}",
"value_template": "{{ value }}",
"friendly_name": "Test",
},
},
},
]
},
)
await hass.async_block_till_done()
entity_state = hass.states.get("cover.test")
assert entity_state
assert entity_state.state == "unknown"
await hass.services.async_call(
COVER_DOMAIN,
SERVICE_OPEN_COVER,
{ATTR_ENTITY_ID: "cover.test"},
blocking=True,
)
entity_state = hass.states.get("cover.test")
assert entity_state
assert entity_state.state == "open"
2021-03-01 16:27:04 +00:00
async def test_no_poll_when_cover_has_no_command_state(hass: HomeAssistant) -> None:
2021-03-01 16:27:04 +00:00
"""Test that the cover does not polls when there's no state command."""
with patch(
"homeassistant.components.command_line.utils.subprocess.check_output",
2021-03-01 16:27:04 +00:00
return_value=b"50\n",
) as check_output:
assert await setup.async_setup_component(
hass,
COVER_DOMAIN,
{
COVER_DOMAIN: [
{"platform": "command_line", "covers": {"test": {}}},
]
},
)
2021-03-01 16:27:04 +00:00
async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL)
await hass.async_block_till_done()
assert not check_output.called
@pytest.mark.parametrize(
"get_config",
[
{
"command_line": [
{
"cover": {
"command_state": "echo state",
"name": "Test",
},
}
]
}
],
)
async def test_poll_when_cover_has_command_state(
hass: HomeAssistant, load_yaml_integration: None
) -> None:
2021-03-01 16:27:04 +00:00
"""Test that the cover polls when there's a state command."""
with patch(
"homeassistant.components.command_line.utils.subprocess.check_output",
2021-03-01 16:27:04 +00:00
return_value=b"50\n",
) as check_output:
async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL)
await hass.async_block_till_done()
check_output.assert_called_once_with(
Avoid subprocess memory copy when c library supports posix_spawn (#87958) * use posix spawn on alpine * Avoid subprocess memory copy when c library supports posix_spawn By default python 3.10 will use the fork() which has to copy all the memory of the parent process (in our case this can be huge since Home Assistant core can use hundreds of megabytes of RAM). By using posix_spawn this is avoided. In python 3.11 vfork will also be available https://github.com/python/cpython/issues/80004#issuecomment-1093810689 https://github.com/python/cpython/pull/11671 but we won't always be able to use it and posix_spawn is considered safer https://bugzilla.kernel.org/show_bug.cgi?id=215813#c14 The subprocess library doesn't know about musl though even though it supports posix_spawn https://git.musl-libc.org/cgit/musl/log/src/process/posix_spawn.c so we have to teach it since it only has checks for glibc https://github.com/python/cpython/blob/1b736838e6ae1b4ef42cdd27c2708face908f92c/Lib/subprocess.py#L745 The constant is documented as being able to be flipped here: https://docs.python.org/3/library/subprocess.html#disabling-use-of-vfork-or-posix-spawn * Avoid subprocess memory copy when c library supports posix_spawn By default python 3.10 will use the fork() which has to copy memory of the parent process (in our case this can be huge since Home Assistant core can use hundreds of megabytes of RAM). By using posix_spawn this is avoided and subprocess creation does not get discernibly slow the larger the Home Assistant python process grows. In python 3.11 vfork will also be available https://github.com/python/cpython/issues/80004#issuecomment-1093810689 https://github.com/python/cpython/pull/11671 but we won't always be able to use it and posix_spawn is considered safer https://bugzilla.kernel.org/show_bug.cgi?id=215813#c14 The subprocess library doesn't know about musl though even though it supports posix_spawn https://git.musl-libc.org/cgit/musl/log/src/process/posix_spawn.c so we have to teach it since it only has checks for glibc https://github.com/python/cpython/blob/1b736838e6ae1b4ef42cdd27c2708face908f92c/Lib/subprocess.py#L745 The constant is documented as being able to be flipped here: https://docs.python.org/3/library/subprocess.html#disabling-use-of-vfork-or-posix-spawn * missed some * adjust more tests * coverage
2023-02-13 14:02:51 +00:00
"echo state",
2023-06-08 20:46:04 +00:00
shell=True, # noqa: S604 # shell by design
Avoid subprocess memory copy when c library supports posix_spawn (#87958) * use posix spawn on alpine * Avoid subprocess memory copy when c library supports posix_spawn By default python 3.10 will use the fork() which has to copy all the memory of the parent process (in our case this can be huge since Home Assistant core can use hundreds of megabytes of RAM). By using posix_spawn this is avoided. In python 3.11 vfork will also be available https://github.com/python/cpython/issues/80004#issuecomment-1093810689 https://github.com/python/cpython/pull/11671 but we won't always be able to use it and posix_spawn is considered safer https://bugzilla.kernel.org/show_bug.cgi?id=215813#c14 The subprocess library doesn't know about musl though even though it supports posix_spawn https://git.musl-libc.org/cgit/musl/log/src/process/posix_spawn.c so we have to teach it since it only has checks for glibc https://github.com/python/cpython/blob/1b736838e6ae1b4ef42cdd27c2708face908f92c/Lib/subprocess.py#L745 The constant is documented as being able to be flipped here: https://docs.python.org/3/library/subprocess.html#disabling-use-of-vfork-or-posix-spawn * Avoid subprocess memory copy when c library supports posix_spawn By default python 3.10 will use the fork() which has to copy memory of the parent process (in our case this can be huge since Home Assistant core can use hundreds of megabytes of RAM). By using posix_spawn this is avoided and subprocess creation does not get discernibly slow the larger the Home Assistant python process grows. In python 3.11 vfork will also be available https://github.com/python/cpython/issues/80004#issuecomment-1093810689 https://github.com/python/cpython/pull/11671 but we won't always be able to use it and posix_spawn is considered safer https://bugzilla.kernel.org/show_bug.cgi?id=215813#c14 The subprocess library doesn't know about musl though even though it supports posix_spawn https://git.musl-libc.org/cgit/musl/log/src/process/posix_spawn.c so we have to teach it since it only has checks for glibc https://github.com/python/cpython/blob/1b736838e6ae1b4ef42cdd27c2708face908f92c/Lib/subprocess.py#L745 The constant is documented as being able to be flipped here: https://docs.python.org/3/library/subprocess.html#disabling-use-of-vfork-or-posix-spawn * missed some * adjust more tests * coverage
2023-02-13 14:02:51 +00:00
timeout=15,
close_fds=False,
)
async def test_state_value(hass: HomeAssistant) -> None:
"""Test with state value."""
with tempfile.TemporaryDirectory() as tempdirname:
2019-07-31 19:25:30 +00:00
path = os.path.join(tempdirname, "cover_status")
await setup.async_setup_component(
2021-03-01 16:27:04 +00:00
hass,
DOMAIN,
2021-03-01 16:27:04 +00:00
{
"command_line": [
{
"cover": {
"command_state": f"cat {path}",
"command_open": f"echo 1 > {path}",
"command_close": f"echo 1 > {path}",
"command_stop": f"echo 0 > {path}",
"value_template": "{{ value }}",
"name": "Test",
}
}
]
2021-03-01 16:27:04 +00:00
},
2019-07-31 19:25:30 +00:00
)
await hass.async_block_till_done()
2021-03-01 16:27:04 +00:00
entity_state = hass.states.get("cover.test")
assert entity_state
assert entity_state.state == "unknown"
await hass.services.async_call(
COVER_DOMAIN,
SERVICE_OPEN_COVER,
{ATTR_ENTITY_ID: "cover.test"},
blocking=True,
2019-07-31 19:25:30 +00:00
)
2021-03-01 16:27:04 +00:00
entity_state = hass.states.get("cover.test")
assert entity_state
assert entity_state.state == "open"
await hass.services.async_call(
COVER_DOMAIN,
SERVICE_CLOSE_COVER,
{ATTR_ENTITY_ID: "cover.test"},
blocking=True,
2019-07-31 19:25:30 +00:00
)
2021-03-01 16:27:04 +00:00
entity_state = hass.states.get("cover.test")
assert entity_state
assert entity_state.state == "open"
await hass.services.async_call(
COVER_DOMAIN,
SERVICE_STOP_COVER,
{ATTR_ENTITY_ID: "cover.test"},
blocking=True,
2019-07-31 19:25:30 +00:00
)
2021-03-01 16:27:04 +00:00
entity_state = hass.states.get("cover.test")
assert entity_state
assert entity_state.state == "closed"
@pytest.mark.parametrize(
"get_config",
[
{
"command_line": [
{
"cover": {
"command_open": "exit 1",
"name": "Test",
}
}
]
}
],
)
async def test_move_cover_failure(
caplog: pytest.LogCaptureFixture, hass: HomeAssistant, load_yaml_integration: None
) -> None:
"""Test command failure."""
2021-03-01 16:27:04 +00:00
await hass.services.async_call(
COVER_DOMAIN, SERVICE_OPEN_COVER, {ATTR_ENTITY_ID: "cover.test"}, blocking=True
2021-03-01 16:27:04 +00:00
)
assert "Command failed" in caplog.text
assert "return code 1" in caplog.text
@pytest.mark.parametrize(
"get_config",
[
{
"command_line": [
{
"cover": {
"command_open": "echo open",
"command_close": "echo close",
"command_stop": "echo stop",
"unique_id": "unique",
"name": "Test",
}
},
{
"cover": {
"command_open": "echo open",
"command_close": "echo close",
"command_stop": "echo stop",
"unique_id": "not-so-unique-anymore",
"name": "Test2",
}
},
{
"cover": {
"command_open": "echo open",
"command_close": "echo close",
"command_stop": "echo stop",
"unique_id": "not-so-unique-anymore",
"name": "Test3",
}
},
]
}
],
)
async def test_unique_id(
hass: HomeAssistant, entity_registry: er.EntityRegistry, load_yaml_integration: None
) -> None:
"""Test unique_id option and if it only creates one cover per id."""
assert len(hass.states.async_all()) == 2
assert len(entity_registry.entities) == 2
assert entity_registry.async_get_entity_id("cover", "command_line", "unique")
assert entity_registry.async_get_entity_id(
"cover", "command_line", "not-so-unique-anymore"
)
async def test_updating_to_often(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test handling updating when command already running."""
called = []
wait_till_event = asyncio.Event()
wait_till_event.set()
class MockCommandCover(CommandCover):
"""Mock entity that updates."""
async def _async_update(self) -> None:
"""Update the entity."""
called.append(1)
# Add waiting time
await wait_till_event.wait()
with patch(
"homeassistant.components.command_line.cover.CommandCover",
side_effect=MockCommandCover,
):
await setup.async_setup_component(
hass,
DOMAIN,
{
"command_line": [
{
"cover": {
"command_state": "echo 1",
"value_template": "{{ value }}",
"name": "Test",
"scan_interval": 10,
}
}
]
},
)
await hass.async_block_till_done()
assert not called
assert (
"Updating Command Line Cover Test took longer than the scheduled update interval"
not in caplog.text
)
async_fire_time_changed(hass, dt_util.now() + timedelta(seconds=11))
await hass.async_block_till_done()
assert called
called.clear()
assert (
"Updating Command Line Cover Test took longer than the scheduled update interval"
not in caplog.text
)
# Simulate update takes too long
wait_till_event.clear()
async_fire_time_changed(hass, dt_util.now() + timedelta(seconds=10))
await asyncio.sleep(0)
async_fire_time_changed(hass, dt_util.now() + timedelta(seconds=10))
wait_till_event.set()
# Finish processing update
await hass.async_block_till_done()
assert called
assert (
"Updating Command Line Cover Test took longer than the scheduled update interval"
in caplog.text
)
async def test_updating_manually(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test handling manual updating using homeassistant udate_entity service."""
await setup.async_setup_component(hass, HA_DOMAIN, {})
called = []
class MockCommandCover(CommandCover):
"""Mock entity that updates."""
async def _async_update(self) -> None:
"""Update."""
called.append(1)
with patch(
"homeassistant.components.command_line.cover.CommandCover",
side_effect=MockCommandCover,
):
await setup.async_setup_component(
hass,
DOMAIN,
{
"command_line": [
{
"cover": {
"command_state": "echo 1",
"value_template": "{{ value }}",
"name": "Test",
"scan_interval": 10,
}
}
]
},
)
await hass.async_block_till_done()
async_fire_time_changed(hass, dt_util.now() + timedelta(seconds=10))
await hass.async_block_till_done()
assert called
called.clear()
await hass.services.async_call(
HA_DOMAIN,
SERVICE_UPDATE_ENTITY,
{ATTR_ENTITY_ID: ["cover.test"]},
blocking=True,
)
await hass.async_block_till_done()
assert called