Skip to content

Commit 7a853c0

Browse files
authored
Merge pull request #517 from sed-i/feature/wait_for_status
#517 null
2 parents 404ef3e + 398ffec commit 7a853c0

5 files changed

Lines changed: 78 additions & 8 deletions

File tree

juju/model.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import re
99
import stat
1010
import tempfile
11+
import warnings
1112
import weakref
1213
import zipfile
1314
from concurrent.futures import CancelledError
@@ -2227,7 +2228,8 @@ async def _get_source_api(self, url, controller_name=None):
22272228
return controller
22282229

22292230
async def wait_for_idle(self, apps=None, raise_on_error=True, raise_on_blocked=False,
2230-
wait_for_active=False, timeout=10 * 60, idle_period=15, check_freq=0.5):
2231+
wait_for_active=False, timeout=10 * 60, idle_period=15, check_freq=0.5,
2232+
status=None):
22312233
"""Wait for applications in the model to settle into an idle state.
22322234
22332235
:param apps (list[str]): Optional list of specific app names to wait on.
@@ -2259,7 +2261,14 @@ async def wait_for_idle(self, apps=None, raise_on_error=True, raise_on_blocked=F
22592261
22602262
:param check_freq (float): How frequently, in seconds, to check the model.
22612263
The default is every half-second.
2264+
2265+
:param status (str): The status to wait for. If None, not waiting.
2266+
The default is None (not waiting for any status).
22622267
"""
2268+
if wait_for_active:
2269+
warnings.warn("wait_for_active is deprecated; use status", DeprecationWarning)
2270+
status = "active"
2271+
22632272
timeout = timedelta(seconds=timeout) if timeout is not None else None
22642273
idle_period = timedelta(seconds=idle_period)
22652274
start_time = datetime.now()
@@ -2311,8 +2320,8 @@ def _raise_for_status(entities, status):
23112320
if raise_on_blocked and unit.workload_status == "blocked":
23122321
blocks.setdefault("Unit", []).append(unit.name)
23132322
continue
2314-
waiting_for_active = wait_for_active and unit.workload_status != "active"
2315-
if not waiting_for_active and unit.agent_status == "idle":
2323+
waiting_for_status = status and unit.workload_status != status
2324+
if not waiting_for_status and unit.agent_status == "idle":
23162325
now = datetime.now()
23172326
idle_start = idle_times.setdefault(unit.name, now)
23182327
if now - idle_start < idle_period:

tests/integration/test_application.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,10 +151,10 @@ async def test_upgrade_local_charm(event_loop):
151151
tests_dir = Path(__file__).absolute().parent
152152
charm_path = tests_dir / 'upgrade-charm'
153153
app = await model.deploy('ubuntu', series='focal')
154-
await model.wait_for_idle(wait_for_active=True)
154+
await model.wait_for_idle(status="active")
155155
assert app.data['charm-url'].startswith('cs:ubuntu')
156156
await app.upgrade_charm(path=charm_path)
157-
await model.wait_for_idle() # nb: can't use wait_for_active because test charm goes to "waiting"
157+
await model.wait_for_idle(status="waiting")
158158
assert app.data['charm-url'] == 'local:focal/ubuntu-0'
159159

160160

tests/integration/test_model.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ async def test_deploy_local_charm(event_loop):
9797
async with base.CleanModel() as model:
9898
await model.deploy(str(charm_path))
9999
assert 'charm' in model.applications
100-
await model.wait_for_idle(wait_for_active=True)
100+
await model.wait_for_idle(status="active")
101101
assert model.units['charm/0'].workload_status == 'active'
102102

103103

@@ -113,7 +113,7 @@ async def test_wait_local_charm_blocked(event_loop):
113113
assert 'charm' in model.applications
114114
await model.wait_for_idle()
115115
with pytest.raises(JujuUnitError):
116-
await model.wait_for_idle(wait_for_active=True,
116+
await model.wait_for_idle(status="active",
117117
raise_on_blocked=True,
118118
timeout=30)
119119

@@ -130,7 +130,7 @@ async def test_wait_local_charm_waiting_timeout(event_loop):
130130
assert 'charm' in model.applications
131131
await model.wait_for_idle()
132132
with pytest.raises(asyncio.TimeoutError):
133-
await model.wait_for_idle(wait_for_active=True, timeout=30)
133+
await model.wait_for_idle(status="active", timeout=30)
134134

135135

136136
@base.bootstrapped

tests/unit/test_model.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import unittest
2+
from unittest.mock import patch, PropertyMock
23

34
import mock
45

6+
import asyncio
57
import asynctest
8+
import datetime
69

710
from juju.client.jujudata import FileJujuData
811
from juju.model import Model
@@ -262,3 +265,50 @@ async def test_with_posargs(self, mock_connect, mock_connect_model, _):
262265
macaroons='macaroons',
263266
loop='loop',
264267
max_frame_size='max_frame_size')
268+
269+
270+
# Patch timedelta to immediately force a timeout to avoid introducing an unnecessary delay in the test failing.
271+
# It should be safe to always set it up to lead to a timeout.
272+
@patch('juju.model.timedelta', new=lambda *a, **kw: datetime.timedelta(0))
273+
class TestModelWaitForIdle(asynctest.TestCase):
274+
async def test_no_args(self):
275+
m = Model()
276+
with self.assertWarns(DeprecationWarning):
277+
# no apps so should return right away
278+
await m.wait_for_idle(wait_for_active=True)
279+
280+
async def test_timeout(self):
281+
m = Model()
282+
with self.assertRaises(asyncio.TimeoutError) as cm:
283+
# no apps so should timeout after timeout period
284+
await m.wait_for_idle(apps=["nonexisting_app"])
285+
self.assertEqual(str(cm.exception), "Timed out waiting for model:\nnonexisting_app (missing)")
286+
287+
async def test_wait_for_active_status(self):
288+
# create a custom apps mock
289+
from types import SimpleNamespace
290+
apps = {"dummy_app": SimpleNamespace(
291+
status="active",
292+
units=[SimpleNamespace(
293+
name="mockunit/0",
294+
workload_status="active",
295+
workload_status_message="workload_status_message",
296+
machine=None,
297+
agent_status="idle",
298+
)],
299+
)}
300+
301+
with patch.object(Model, 'applications', new_callable=PropertyMock) as mock_apps:
302+
mock_apps.return_value = apps
303+
m = Model()
304+
305+
# pass "active" via `status` (str)
306+
await m.wait_for_idle(apps=["dummy_app"], status="active")
307+
308+
# pass "active" via `wait_for_active` (bool; deprecated)
309+
await m.wait_for_idle(apps=["dummy_app"], wait_for_active=True)
310+
311+
# use both `status` and `wait_for_active` - `wait_for_active` takes precedence
312+
await m.wait_for_idle(apps=["dummy_app"], wait_for_active=True, status="doesn't matter")
313+
314+
mock_apps.assert_called_with()

tox.ini

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ skipsdist=True
1010
[pytest]
1111
markers =
1212
serial: mark a test that must run by itself
13+
filterwarnings =
14+
ignore::DeprecationWarning:asynctest
15+
ignore::DeprecationWarning:websockets
1316

1417
[testenv]
1518
usedevelop=True
@@ -49,6 +52,14 @@ commands =
4952
pip install pylxd
5053
py.test --tb native -ra -v -n auto -k 'integration' -m 'not serial' {posargs}
5154

55+
[testenv:unit]
56+
envdir = {toxworkdir}/py3
57+
commands =
58+
# These need to be installed in a specific order
59+
pip install urllib3==1.25.7
60+
pip install pylxd
61+
py.test --tb native -ra -v -n auto {toxinidir}/tests/unit {posargs}
62+
5263
[testenv:serial]
5364
# tests that can't be run in parallel
5465
envdir = {toxworkdir}/py3

0 commit comments

Comments
 (0)