From 72d81e43dd0b091c69c8333c218afb8182dacbab Mon Sep 17 00:00:00 2001 From: Mike Degatano Date: Wed, 18 Jan 2023 06:14:12 -0500 Subject: [PATCH] Allow all job conditions to be ignored (#4107) * Allow all job conditions to be ignored * Clear features cache in test * patch out OS Agent supported feature --- supervisor/jobs/decorator.py | 16 ++-- tests/jobs/test_job_decorator.py | 121 ++++++++++++++++++------------- 2 files changed, 77 insertions(+), 60 deletions(-) diff --git a/supervisor/jobs/decorator.py b/supervisor/jobs/decorator.py index 3acb73c44..abe951e6b 100644 --- a/supervisor/jobs/decorator.py +++ b/supervisor/jobs/decorator.py @@ -199,14 +199,14 @@ class Job(CoreSysAttributes): f"'{self._method.__qualname__}' blocked from execution, not enough free space ({self.sys_host.info.free_space}GB) left on the device" ) - if JobCondition.INTERNET_SYSTEM in self.conditions: + if JobCondition.INTERNET_SYSTEM in used_conditions: await self.sys_supervisor.check_connectivity() if not self.sys_supervisor.connectivity: raise JobConditionException( f"'{self._method.__qualname__}' blocked from execution, no supervisor internet connection" ) - if JobCondition.INTERNET_HOST in self.conditions: + if JobCondition.INTERNET_HOST in used_conditions: await self.sys_host.network.check_connectivity() if ( self.sys_host.network.connectivity is not None @@ -216,13 +216,13 @@ class Job(CoreSysAttributes): f"'{self._method.__qualname__}' blocked from execution, no host internet connection" ) - if JobCondition.HAOS in self.conditions and not self.sys_os.available: + if JobCondition.HAOS in used_conditions and not self.sys_os.available: raise JobConditionException( f"'{self._method.__qualname__}' blocked from execution, no Home Assistant OS available" ) if ( - JobCondition.OS_AGENT in self.conditions + JobCondition.OS_AGENT in used_conditions and HostFeature.OS_AGENT not in self.sys_host.features ): raise JobConditionException( @@ -230,7 +230,7 @@ class Job(CoreSysAttributes): ) if ( - JobCondition.HOST_NETWORK in self.conditions + JobCondition.HOST_NETWORK in used_conditions and not self.sys_dbus.network.is_connected ): raise JobConditionException( @@ -238,7 +238,7 @@ class Job(CoreSysAttributes): ) if ( - JobCondition.AUTO_UPDATE in self.conditions + JobCondition.AUTO_UPDATE in used_conditions and not self.sys_updater.auto_update ): raise JobConditionException( @@ -246,14 +246,14 @@ class Job(CoreSysAttributes): ) if ( - JobCondition.SUPERVISOR_UPDATED in self.conditions + JobCondition.SUPERVISOR_UPDATED in used_conditions and self.sys_supervisor.need_update ): raise JobConditionException( f"'{self._method.__qualname__}' blocked from execution, supervisor needs to be updated first" ) - if JobCondition.PLUGINS_UPDATED in self.conditions and ( + if JobCondition.PLUGINS_UPDATED in used_conditions and ( out_of_date := [ plugin for plugin in self.sys_plugins.all_plugins if plugin.need_update ] diff --git a/tests/jobs/test_job_decorator.py b/tests/jobs/test_job_decorator.py index c3f8ca296..22903050e 100644 --- a/tests/jobs/test_job_decorator.py +++ b/tests/jobs/test_job_decorator.py @@ -16,13 +16,16 @@ from supervisor.exceptions import ( JobException, PluginJobError, ) +from supervisor.host.const import HostFeature +from supervisor.host.manager import HostManager from supervisor.jobs.const import JobExecutionLimit from supervisor.jobs.decorator import Job, JobCondition +from supervisor.plugins.audio import PluginAudio from supervisor.resolution.const import UnhealthyReason from supervisor.utils.dt import utcnow -async def test_healthy(coresys: CoreSys): +async def test_healthy(coresys: CoreSys, caplog: pytest.LogCaptureFixture): """Test the healty decorator.""" class TestClass: @@ -42,6 +45,10 @@ async def test_healthy(coresys: CoreSys): coresys.resolution.unhealthy = UnhealthyReason.DOCKER assert not await test.execute() + assert "blocked from execution, system is not healthy - docker" in caplog.text + + coresys.jobs.ignore_conditions = [JobCondition.HEALTHY] + assert await test.execute() @pytest.mark.parametrize( @@ -93,6 +100,13 @@ async def test_internet( assert await test.execute_host() is host_result assert await test.execute_system() is system_result + coresys.jobs.ignore_conditions = [ + JobCondition.INTERNET_HOST, + JobCondition.INTERNET_SYSTEM, + ] + assert await test.execute_host() + assert await test.execute_system() + async def test_free_space(coresys: CoreSys): """Test the free_space decorator.""" @@ -116,6 +130,9 @@ async def test_free_space(coresys: CoreSys): with patch("shutil.disk_usage", return_value=(42, 42, (512.0**3))): assert not await test.execute() + coresys.jobs.ignore_conditions = [JobCondition.FREE_SPACE] + assert await test.execute() + async def test_haos(coresys: CoreSys): """Test the haos decorator.""" @@ -139,9 +156,12 @@ async def test_haos(coresys: CoreSys): coresys.os._available = False assert not await test.execute() + coresys.jobs.ignore_conditions = [JobCondition.HAOS] + assert await test.execute() -async def test_exception(coresys: CoreSys): - """Test the healty decorator.""" + +async def test_exception(coresys: CoreSys, capture_exception: Mock): + """Test handled exception.""" class TestClass: """Test class.""" @@ -160,9 +180,12 @@ async def test_exception(coresys: CoreSys): with pytest.raises(HassioError): assert await test.execute() + capture_exception.assert_not_called() -async def test_exception_not_handle(coresys: CoreSys): - """Test the healty decorator.""" + +async def test_exception_not_handle(coresys: CoreSys, capture_exception: Mock): + """Test unhandled exception.""" + err = Exception() class TestClass: """Test class.""" @@ -174,13 +197,15 @@ async def test_exception_not_handle(coresys: CoreSys): @Job(conditions=[JobCondition.HEALTHY]) async def execute(self): """Execute the class method.""" - raise Exception() + raise err test = TestClass(coresys) with pytest.raises(JobException): assert await test.execute() + capture_exception.assert_called_once_with(err) + async def test_running(coresys: CoreSys): """Test the running decorator.""" @@ -205,30 +230,6 @@ async def test_running(coresys: CoreSys): coresys.core.state = CoreState.FREEZE assert not await test.execute() - -async def test_ignore_conditions(coresys: CoreSys): - """Test the ignore conditions decorator.""" - - class TestClass: - """Test class.""" - - def __init__(self, coresys: CoreSys): - """Initialize the test class.""" - self.coresys = coresys - - @Job(conditions=[JobCondition.RUNNING]) - async def execute(self): - """Execute the class method.""" - return True - - test = TestClass(coresys) - - coresys.core.state = CoreState.RUNNING - assert await test.execute() - - coresys.core.state = CoreState.FREEZE - assert not await test.execute() - coresys.jobs.ignore_conditions = [JobCondition.RUNNING] assert await test.execute() @@ -258,7 +259,7 @@ async def test_exception_conditions(coresys: CoreSys): await test.execute() -async def test_exectution_limit_single_wait( +async def test_execution_limit_single_wait( coresys: CoreSys, loop: asyncio.BaseEventLoop ): """Test the single wait job execution limit.""" @@ -355,7 +356,7 @@ async def test_execution_limit_throttle_rate_limit( assert test.call == 3 -async def test_exectution_limit_throttle(coresys: CoreSys, loop: asyncio.BaseEventLoop): +async def test_execution_limit_throttle(coresys: CoreSys, loop: asyncio.BaseEventLoop): """Test the ignore conditions decorator.""" class TestClass: @@ -384,7 +385,7 @@ async def test_exectution_limit_throttle(coresys: CoreSys, loop: asyncio.BaseEve assert test.call == 1 -async def test_exectution_limit_once(coresys: CoreSys, loop: asyncio.BaseEventLoop): +async def test_execution_limit_once(coresys: CoreSys, loop: asyncio.BaseEventLoop): """Test the ignore conditions decorator.""" class TestClass: @@ -436,6 +437,9 @@ async def test_supervisor_updated(coresys: CoreSys): ): assert not await test.execute() + coresys.jobs.ignore_conditions = [JobCondition.SUPERVISOR_UPDATED] + assert await test.execute() + async def test_plugins_updated(coresys: CoreSys): """Test the plugins updated decorator.""" @@ -459,13 +463,16 @@ async def test_plugins_updated(coresys: CoreSys): assert await test.execute() with patch.object( - type(coresys.plugins.audio), "need_update", new=PropertyMock(return_value=True) + PluginAudio, "need_update", new=PropertyMock(return_value=True) ), patch.object( - type(coresys.plugins.audio), "update", side_effect=[AudioUpdateError, None] + PluginAudio, "update", side_effect=[AudioUpdateError, None, AudioUpdateError] ): assert not await test.execute() assert await test.execute() + coresys.jobs.ignore_conditions = [JobCondition.PLUGINS_UPDATED] + assert await test.execute() + async def test_auto_update(coresys: CoreSys): """Test the auto update decorator.""" @@ -489,9 +496,12 @@ async def test_auto_update(coresys: CoreSys): coresys.updater.auto_update = False assert not await test.execute() + coresys.jobs.ignore_conditions = [JobCondition.AUTO_UPDATE] + assert await test.execute() -async def test_unhealthy(coresys: CoreSys, caplog: pytest.LogCaptureFixture): - """Test the healthy decorator when unhealthy.""" + +async def test_os_agent(coresys: CoreSys): + """Test the os agent decorator.""" class TestClass: """Test class.""" @@ -500,23 +510,27 @@ async def test_unhealthy(coresys: CoreSys, caplog: pytest.LogCaptureFixture): """Initialize the test class.""" self.coresys = coresys - @Job(conditions=[JobCondition.HEALTHY]) + @Job(conditions=[JobCondition.OS_AGENT]) async def execute(self) -> bool: """Execute the class method.""" return True test = TestClass(coresys) - coresys.resolution.unhealthy = UnhealthyReason.SETUP - assert not await test.execute() - assert "blocked from execution, system is not healthy - setup" in caplog.text + with patch.object( + HostManager, "supported_features", return_value=[HostFeature.OS_AGENT] + ): + assert await test.execute() - coresys.jobs.ignore_conditions = [JobCondition.HEALTHY] - assert await test.execute() + coresys.host.supported_features.cache_clear() + with patch.object(HostManager, "supported_features", return_value=[]): + assert not await test.execute() + + coresys.jobs.ignore_conditions = [JobCondition.OS_AGENT] + assert await test.execute() -async def test_unhandled_exception(coresys: CoreSys, capture_exception: Mock): - """Test an unhandled exception from job.""" - err = OSError() +async def test_host_network(coresys: CoreSys): + """Test the host network decorator.""" class TestClass: """Test class.""" @@ -525,13 +539,16 @@ async def test_unhandled_exception(coresys: CoreSys, capture_exception: Mock): """Initialize the test class.""" self.coresys = coresys - @Job(conditions=[JobCondition.HEALTHY]) - async def execute(self) -> None: + @Job(conditions=[JobCondition.HOST_NETWORK]) + async def execute(self) -> bool: """Execute the class method.""" - raise err + return True test = TestClass(coresys) - with pytest.raises(JobException): - await test.execute() + assert await test.execute() - capture_exception.assert_called_once_with(err) + coresys.dbus.network.disconnect() + assert not await test.execute() + + coresys.jobs.ignore_conditions = [JobCondition.HOST_NETWORK] + assert await test.execute()