Skip to content

Commit 7fe482a

Browse files
committed
Fix both integration tests and make them handle incompatible upgrades
Signed-off-by: Marcelo Henrique Neppel <marcelo.neppel@canonical.com>
1 parent c40d26e commit 7fe482a

File tree

5 files changed

+106
-33
lines changed

5 files changed

+106
-33
lines changed

src/charm.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,7 @@ def __init__(self, *args):
332332

333333
if (
334334
self.refresh is not None
335+
and self.refresh.workload_allowed_to_start
335336
and not self.refresh.next_unit_allowed_to_refresh
336337
and int(self.unit.name.split("/")[1]) >= self.refresh._get_partition()
337338
):

src/refresh.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,9 @@ def is_compatible(
3838
old_workload_version=old_workload_version,
3939
new_workload_version=new_workload_version,
4040
):
41-
logger.error("Charm version is not compatible 1")
41+
logger.error(
42+
f"Charm version is not compatible 1 - old_charm_version: {old_charm_version}, new_charm_version: {new_charm_version}, old_workload_version: {old_workload_version}, new_workload_version: {new_workload_version}"
43+
)
4244
return False
4345

4446
# Check workload version compatibility

tests/integration/ha_tests/test_upgrade.py

Lines changed: 42 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import shutil
77
import zipfile
88
from pathlib import Path
9+
from time import sleep
910

1011
import pytest
1112
import tomli
@@ -128,10 +129,6 @@ async def test_upgrade_from_edge(ops_test: OpsTest, charm, continuous_writes) ->
128129
await application.refresh(path=charm, resources=resources)
129130

130131
logger.info("Wait for upgrade to start")
131-
132-
# Blocked status is expected due to:
133-
# (on PR) compatibility checks (on PR charm revision is '16/1.25.0+dirty...')
134-
# (non-PR) the first unit upgraded and paused (pause-after-unit-refresh=first)
135132
await ops_test.model.block_until(lambda: application.status == "blocked", timeout=60 * 3)
136133

137134
logger.info("Wait for refresh to block as paused or incompatible")
@@ -144,23 +141,34 @@ async def test_upgrade_from_edge(ops_test: OpsTest, charm, continuous_writes) ->
144141
refresh_order = sorted(
145142
application.units, key=lambda unit: int(unit.name.split("/")[1]), reverse=True
146143
)
147-
if "Refresh incompatible" in application.status_message:
144+
145+
if "Refresh incompatible" in refresh_order[0].workload_status_message:
148146
logger.info("Application refresh is blocked due to incompatibility")
149147

150148
action = await refresh_order[0].run_action(
151149
"force-refresh-start", **{"check-compatibility": False}
152150
)
153151
await action.wait()
154152

155-
logger.info("Wait for first incompatible unit to upgrade")
156-
async with ops_test.fast_forward("60s"):
157-
await ops_test.model.wait_for_idle(
158-
apps=[DATABASE_APP_NAME], idle_period=30, timeout=TIMEOUT
159-
)
153+
logger.info("Wait for first incompatible unit to upgrade")
154+
async with ops_test.fast_forward("60s"):
155+
await ops_test.model.wait_for_idle(
156+
apps=[DATABASE_APP_NAME], idle_period=30, timeout=TIMEOUT
157+
)
158+
else:
159+
async with ops_test.fast_forward("60s"):
160+
await ops_test.model.block_until(
161+
lambda: all(unit.workload_status == "active" for unit in application.units),
162+
timeout=60 * 3,
163+
)
160164

161-
logger.info("Run resume-refresh action")
162-
action = await refresh_order[1].run_action("resume-refresh")
165+
sleep(60)
166+
167+
leader_unit = await get_leader_unit(ops_test, DATABASE_APP_NAME)
168+
logger.info(f"Run resume-refresh action on {leader_unit.name}")
169+
action = await leader_unit.run_action("resume-refresh")
163170
await action.wait()
171+
logger.info(f"Results from the action: {action.results}")
164172

165173
logger.info("Wait for upgrade to complete")
166174
async with ops_test.fast_forward("60s"):
@@ -227,9 +235,14 @@ async def test_fail_and_rollback(ops_test, charm, continuous_writes) -> None:
227235

228236
logger.info("Wait for upgrade to fail")
229237

238+
# Highest to lowest unit number
239+
refresh_order = sorted(
240+
application.units, key=lambda unit: int(unit.name.split("/")[1]), reverse=True
241+
)
242+
230243
await ops_test.model.block_until(
231244
lambda: application.status == "blocked"
232-
and "incompatible" in application.status_message.lower(),
245+
and "Refresh incompatible" in refresh_order[0].workload_status_message,
233246
timeout=TIMEOUT,
234247
)
235248

@@ -244,9 +257,24 @@ async def test_fail_and_rollback(ops_test, charm, continuous_writes) -> None:
244257
await ops_test.model.block_until(lambda: application.status == "blocked", timeout=TIMEOUT)
245258

246259
logger.info("Wait for application to recover")
260+
async with ops_test.fast_forward("60s"):
261+
await ops_test.model.block_until(
262+
lambda: all(unit.workload_status == "active" for unit in application.units),
263+
timeout=60 * 3,
264+
)
265+
266+
sleep(60)
267+
268+
leader_unit = await get_leader_unit(ops_test, DATABASE_APP_NAME)
269+
logger.info(f"Run resume-refresh action on {leader_unit.name}")
270+
action = await leader_unit.run_action("resume-refresh")
271+
await action.wait()
272+
logger.info(f"Results from the action: {action.results}")
273+
274+
logger.info("Wait for upgrade to complete")
247275
async with ops_test.fast_forward("60s"):
248276
await ops_test.model.wait_for_idle(
249-
apps=[DATABASE_APP_NAME], status="active", timeout=TIMEOUT
277+
apps=[DATABASE_APP_NAME], status="active", idle_period=30, timeout=TIMEOUT
250278
)
251279

252280
logger.info("Ensure continuous_writes after rollback procedure")

tests/integration/ha_tests/test_upgrade_from_stable.py

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -123,17 +123,36 @@ async def test_upgrade_from_stable(ops_test: OpsTest, charm):
123123
logger.info("Wait for upgrade to start")
124124
await ops_test.model.block_until(lambda: application.status == "blocked", timeout=60 * 3)
125125

126-
logger.info("Wait for refresh to block as paused")
126+
logger.info("Wait for refresh to block as paused or incompatible")
127127
async with ops_test.fast_forward("60s"):
128128
await ops_test.model.wait_for_idle(
129129
apps=[DATABASE_APP_NAME], idle_period=30, timeout=TIMEOUT
130130
)
131131

132-
async with ops_test.fast_forward("60s"):
133-
await ops_test.model.block_until(
134-
lambda: all(unit.workload_status == "active" for unit in application.units),
135-
timeout=60 * 3,
132+
# Highest to lowest unit number
133+
refresh_order = sorted(
134+
application.units, key=lambda unit: int(unit.name.split("/")[1]), reverse=True
135+
)
136+
137+
if "Refresh incompatible" in application.status_message:
138+
logger.info("Application refresh is blocked due to incompatibility")
139+
140+
action = await refresh_order[0].run_action(
141+
"force-refresh-start", **{"check-compatibility": False}
136142
)
143+
await action.wait()
144+
145+
logger.info("Wait for first incompatible unit to upgrade")
146+
async with ops_test.fast_forward("60s"):
147+
await ops_test.model.wait_for_idle(
148+
apps=[DATABASE_APP_NAME], idle_period=30, timeout=TIMEOUT
149+
)
150+
else:
151+
async with ops_test.fast_forward("60s"):
152+
await ops_test.model.block_until(
153+
lambda: all(unit.workload_status == "active" for unit in application.units),
154+
timeout=60 * 3,
155+
)
137156

138157
sleep(60)
139158

tests/unit/conftest.py

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Copyright 2023 Canonical Ltd.
22
# See LICENSE file for licensing details.
3-
from unittest.mock import PropertyMock
3+
from unittest.mock import Mock, PropertyMock, patch
44

55
import pytest
66
from charms.tempo_coordinator_k8s.v0.charm_tracing import charm_tracing_disabled
@@ -19,20 +19,43 @@ def disable_charm_tracing():
1919
yield
2020

2121

22-
class _MockRefresh:
23-
in_progress = False
24-
next_unit_allowed_to_refresh = True
25-
workload_allowed_to_start = True
26-
app_status_higher_priority = None
27-
unit_status_higher_priority = None
22+
# class _MockRefresh:
23+
# in_progress = False
24+
# next_unit_allowed_to_refresh = True
25+
# workload_allowed_to_start = True
26+
# app_status_higher_priority = None
27+
# unit_status_higher_priority = None
28+
#
29+
# def __init__(self, _, /):
30+
# pass
31+
#
32+
# def unit_status_lower_priority(self, *, workload_is_running=True):
33+
# return None
2834

29-
def __init__(self, _, /):
30-
pass
3135

32-
def unit_status_lower_priority(self, *, workload_is_running=True):
33-
return None
36+
# @pytest.fixture(autouse=True)
37+
# def patch(monkeypatch):
38+
# monkeypatch.setattr("charm_refresh.Kubernetes", _MockRefresh)
3439

3540

3641
@pytest.fixture(autouse=True)
37-
def patch(monkeypatch):
38-
monkeypatch.setattr("charm_refresh.Kubernetes", _MockRefresh)
42+
def mock_refresh():
43+
"""Fixture to shunt refresh logic and events."""
44+
refresh_mock = Mock()
45+
refresh_mock.in_progress = False
46+
refresh_mock.unit_status_higher_priority = None
47+
refresh_mock.unit_status_lower_priority.return_value = None
48+
refresh_mock.next_unit_allowed_to_refresh = True
49+
refresh_mock.workload_allowed_to_start = True
50+
51+
# Mock the _RefreshVersions class to avoid KeyError when charm key is missing
52+
versions_mock = Mock()
53+
versions_mock.charm = "v1/4.0.0"
54+
versions_mock.workload = "4.0.0"
55+
56+
with (
57+
patch("charm_refresh.Kubernetes", Mock(return_value=refresh_mock)),
58+
patch("charm.PostgreSQLRefresh", Mock(return_value=None)),
59+
patch("charm_refresh._main._RefreshVersions", Mock(return_value=versions_mock)),
60+
):
61+
yield

0 commit comments

Comments
 (0)