From 2a5ac125be55c1e7583df0e1f373f2768699640a Mon Sep 17 00:00:00 2001 From: marcoppenheimer <51744472+marcoppenheimer@users.noreply.github.com> Date: Wed, 19 Jul 2023 15:53:15 +0100 Subject: [PATCH] DPE-2247 - test: add upgrade unit-tests (#78) --- tests/unit/test_upgrade.py | 538 ++++++++++++++++++++++++++++++++++++- tox.ini | 1 + 2 files changed, 532 insertions(+), 7 deletions(-) diff --git a/tests/unit/test_upgrade.py b/tests/unit/test_upgrade.py index ed8cb37d..a5344f78 100644 --- a/tests/unit/test_upgrade.py +++ b/tests/unit/test_upgrade.py @@ -1,12 +1,19 @@ # Copyright 2023 Canonical Ltd. # See LICENSE file for licensing details. +import json +import logging + import pytest +from ops.charm import CharmBase +from ops.testing import Harness from pydantic import ValidationError from charms.data_platform_libs.v0.upgrade import ( BaseModel, + DataUpgrade, DependencyModel, + VersionError, build_complete_sem_ver, verify_caret_requirements, verify_inequality_requirements, @@ -14,11 +21,57 @@ verify_wildcard_requirements, ) +logger = logging.getLogger(__name__) + +GANDALF_METADATA = """ +name: gandalf +peers: + upgrade: + interface: upgrade +""" + +GANDALF_ACTIONS = """ +pre-upgrade-check: + description: "YOU SHALL NOT PASS" +""" + +GANDALF_DEPS = { + "gandalf_the_white": { + "dependencies": {"gandalf_the_grey": ">5"}, + "name": "gandalf", + "upgrade_supported": ">1.2", + "version": "7", + }, +} + class GandalfModel(BaseModel): gandalf_the_white: DependencyModel +class GandalfUpgrade(DataUpgrade): + def pre_upgrade_check(self): + pass + + def log_rollback_instructions(self): + pass + + def _on_upgrade_granted(self, _): + pass + + +class GandalfCharm(CharmBase): + def __init__(self, *args): + super().__init__(*args) + + +@pytest.fixture +def harness(): + harness = Harness(GandalfCharm, meta=GANDALF_METADATA, actions=GANDALF_ACTIONS) + harness.begin() + return harness + + @pytest.mark.parametrize( "version,output", [ @@ -245,9 +298,13 @@ def test_dependency_model_raises_for_bad_upgrade_supported(value): def test_dependency_model_succeeds(): + GandalfModel(**GANDALF_DEPS) + + +def test_dependency_model_succeeds_nested(): deps = { "gandalf_the_white": { - "dependencies": {"gandalf_the_grey": ">5"}, + "dependencies": {"gandalf_the_grey": "~1.0", "durin": "^1.2.5"}, "name": "gandalf", "upgrade_supported": ">1.2", "version": "7", @@ -257,14 +314,481 @@ def test_dependency_model_succeeds(): GandalfModel(**deps) -def test_dependency_model_succeeds_nested(): - deps = { +@pytest.mark.parametrize( + "min_state", + [ + ("failed"), + ("idle"), + ("ready"), + ("upgrading"), + ("completed"), + ], +) +def test_cluster_state(harness, min_state): + gandalf_model = GandalfModel(**GANDALF_DEPS) + harness.charm.upgrade = GandalfUpgrade( + charm=harness.charm, dependency_model=gandalf_model, substrate="k8s" + ) + harness.add_relation("upgrade", "gandalf") + + harness.set_leader(True) + + harness.add_relation_unit(harness.charm.upgrade.peer_relation.id, "gandalf/1") + + with harness.hooks_disabled(): + harness.update_relation_data( + harness.charm.upgrade.peer_relation.id, "gandalf/0", {"state": min_state} + ) + harness.update_relation_data( + harness.charm.upgrade.peer_relation.id, "gandalf/1", {"state": "completed"} + ) + + assert harness.charm.upgrade.cluster_state == min_state + + +def test_data_upgrade_raises_on_init(harness): + # nothing implemented + class GandalfUpgrade(DataUpgrade): + pass + + with pytest.raises(TypeError): + GandalfUpgrade(charm=harness.charm, dependency_model=GandalfModel) + + # missing pre-upgrade-check + class GandalfUpgrade(DataUpgrade): + def log_rollback_instructions(self): + pass + + def _on_upgrade_granted(self, _): + pass + + with pytest.raises(TypeError): + GandalfUpgrade(charm=harness.charm, dependency_model=GandalfModel) + + # missing missing log-rollback-instructions + class GandalfUpgrade(DataUpgrade): + def pre_upgrade_check(self): + pass + + def _on_upgrade_granted(self, _): + pass + + with pytest.raises(TypeError): + GandalfUpgrade(charm=harness.charm, dependency_model=GandalfModel) + + # missing on-upgrade-granted + class GandalfUpgrade(DataUpgrade): + def pre_upgrade_check(self): + pass + + def log_rollback_instructions(self): + pass + + with pytest.raises(TypeError): + GandalfUpgrade(charm=harness.charm, dependency_model=GandalfModel) + + +def test_data_upgrade_succeeds(harness): + GandalfUpgrade(charm=harness.charm, dependency_model=GandalfModel) + + +def test_build_upgrade_stack_raises_not_implemented_vm(harness): + gandalf = GandalfUpgrade(charm=harness.charm, dependency_model=GandalfModel) + with pytest.raises(NotImplementedError): + gandalf.build_upgrade_stack() + + +def test_build_upgrade_stack_succeeds_k8s(harness): + gandalf = GandalfUpgrade(charm=harness.charm, dependency_model=GandalfModel, substrate="k8s") + gandalf.build_upgrade_stack() + + +def test_set_unit_failed_resets_stack(harness): + gandalf = GandalfUpgrade(charm=harness.charm, dependency_model=GandalfModel, substrate="k8s") + harness.add_relation("upgrade", "gandalf") + gandalf._upgrade_stack = ["1", "2", "3"] + harness.set_leader(True) + + assert gandalf._upgrade_stack + + gandalf.set_unit_failed() + + assert not gandalf._upgrade_stack + + +def test_set_unit_completed_resets_stack(harness): + gandalf = GandalfUpgrade(charm=harness.charm, dependency_model=GandalfModel, substrate="k8s") + harness.add_relation("upgrade", "gandalf") + gandalf._upgrade_stack = ["1", "2", "3"] + harness.set_leader(True) + + assert gandalf._upgrade_stack + + gandalf.set_unit_completed() + + assert not gandalf._upgrade_stack + + +def test_upgrade_created_sets_idle_and_deps(harness): + gandalf_model = GandalfModel(**GANDALF_DEPS) + harness.charm.upgrade = GandalfUpgrade( + charm=harness.charm, dependency_model=gandalf_model, substrate="k8s" + ) + harness.set_leader(True) + + # relation-created + harness.add_relation("upgrade", "gandalf") + + assert harness.charm.upgrade.peer_relation + assert harness.charm.upgrade.peer_relation.data[harness.charm.unit].get("state") == "idle" + assert ( + json.loads( + harness.charm.upgrade.peer_relation.data[harness.charm.app].get("dependencies", "") + ) + == GANDALF_DEPS + ) + + +def test_pre_upgrade_check_action_fails_non_leader(harness, mocker): + gandalf_model = GandalfModel(**GANDALF_DEPS) + harness.charm.upgrade = GandalfUpgrade( + charm=harness.charm, dependency_model=gandalf_model, substrate="k8s" + ) + harness.add_relation("upgrade", "gandalf") + + mock_event = mocker.MagicMock() + harness.charm.upgrade._on_pre_upgrade_check_action(mock_event) + + mock_event.fail.assert_called_once() + + +def test_pre_upgrade_check_action_fails_already_upgrading(harness, mocker): + gandalf_model = GandalfModel(**GANDALF_DEPS) + harness.charm.upgrade = GandalfUpgrade( + charm=harness.charm, dependency_model=gandalf_model, substrate="k8s" + ) + harness.add_relation("upgrade", "gandalf") + + harness.set_leader(True) + harness.charm.upgrade.peer_relation.data[harness.charm.unit].update({"state": "ready"}) + + mock_event = mocker.MagicMock() + harness.charm.upgrade._on_pre_upgrade_check_action(mock_event) + + mock_event.fail.assert_called_once() + + +def test_pre_upgrade_check_action_runs_pre_upgrade_checks(harness, mocker): + gandalf_model = GandalfModel(**GANDALF_DEPS) + harness.charm.upgrade = GandalfUpgrade( + charm=harness.charm, dependency_model=gandalf_model, substrate="k8s" + ) + harness.add_relation("upgrade", "gandalf") + + harness.set_leader(True) + + mocker.patch.object(harness.charm.upgrade, "pre_upgrade_check") + mock_event = mocker.MagicMock() + harness.charm.upgrade._on_pre_upgrade_check_action(mock_event) + + harness.charm.upgrade.pre_upgrade_check.assert_called_once() + + +def test_pre_upgrade_check_action_builds_upgrade_stack_vm(harness, mocker): + gandalf_model = GandalfModel(**GANDALF_DEPS) + harness.charm.upgrade = GandalfUpgrade(charm=harness.charm, dependency_model=gandalf_model) + harness.add_relation("upgrade", "gandalf") + + harness.set_leader(True) + + mocker.patch.object(harness.charm.upgrade, "build_upgrade_stack", return_value=[1, 2, 3]) + mock_event = mocker.MagicMock() + harness.charm.upgrade._on_pre_upgrade_check_action(mock_event) + + harness.charm.upgrade.build_upgrade_stack.assert_called_once() + + relation_stack = harness.charm.upgrade.peer_relation.data[harness.charm.app].get( + "upgrade-stack", "" + ) + + assert relation_stack + assert json.loads(relation_stack) == harness.charm.upgrade.upgrade_stack + assert json.loads(relation_stack) == [1, 2, 3] + + +def test_pre_upgrade_check_action_builds_upgrade_stack_k8s(harness, mocker): + gandalf_model = GandalfModel(**GANDALF_DEPS) + harness.charm.upgrade = GandalfUpgrade( + charm=harness.charm, dependency_model=gandalf_model, substrate="k8s" + ) + harness.add_relation("upgrade", "gandalf") + + harness.set_leader(True) + + harness.add_relation_unit(harness.charm.upgrade.peer_relation.id, "gandalf/1") + harness.update_relation_data( + harness.charm.upgrade.peer_relation.id, "gandalf/1", {"state": "idle"} + ) + + mock_event = mocker.MagicMock() + harness.charm.upgrade._on_pre_upgrade_check_action(mock_event) + + relation_stack = harness.charm.upgrade.peer_relation.data[harness.charm.app].get( + "upgrade-stack", "" + ) + + assert relation_stack + assert json.loads(relation_stack) == harness.charm.upgrade.upgrade_stack + assert json.loads(relation_stack) == [0, 1] + + +def test_upgrade_suppported_check_fails(harness): + bad_deps = { "gandalf_the_white": { - "dependencies": {"gandalf_the_grey": "~1.0", "durin": "^1.2.5"}, + "dependencies": {"gandalf_the_grey": ">5"}, "name": "gandalf", - "upgrade_supported": ">1.2", - "version": "7", + "upgrade_supported": "~0.2", + "version": "0.2.1", }, } + harness.charm.upgrade = GandalfUpgrade( + charm=harness.charm, dependency_model=GandalfModel(**GANDALF_DEPS), substrate="k8s" + ) - GandalfModel(**deps) + harness.add_relation("upgrade", "gandalf") + + harness.update_relation_data( + harness.charm.upgrade.peer_relation.id, "gandalf", {"dependencies": json.dumps(bad_deps)} + ) + + with pytest.raises(VersionError): + harness.charm.upgrade._upgrade_supported_check() + + +def test_upgrade_suppported_check_succeeds(harness): + good_deps = { + "gandalf_the_white": { + "dependencies": {"gandalf_the_grey": ">5"}, + "name": "gandalf", + "upgrade_supported": ">0.2", + "version": "1.3", + }, + } + harness.charm.upgrade = GandalfUpgrade( + charm=harness.charm, dependency_model=GandalfModel(**GANDALF_DEPS), substrate="k8s" + ) + + harness.add_relation("upgrade", "gandalf") + + harness.update_relation_data( + harness.charm.upgrade.peer_relation.id, "gandalf", {"dependencies": json.dumps(good_deps)} + ) + + harness.charm.upgrade._upgrade_supported_check() + + +def test_upgrade_charm_sets_failed_and_logs(harness, mocker): + harness.charm.upgrade = GandalfUpgrade( + charm=harness.charm, dependency_model=GandalfModel(**GANDALF_DEPS), substrate="k8s" + ) + harness.add_relation("upgrade", "gandalf") + + # if not upgrade stack + mocker.patch.object(harness.charm.upgrade, "log_rollback_instructions") + mocker.patch.object(harness.charm.upgrade, "set_unit_failed") + harness.charm.on.upgrade_charm.emit() + + harness.charm.upgrade.log_rollback_instructions.assert_called_once() + harness.charm.upgrade.set_unit_failed.assert_called_once() + + # if cluster state failed + harness.charm.upgrade.peer_relation.data[harness.charm.unit].update({"state": "failed"}) + harness.charm.upgrade.upgrade_stack = [0] + + mocker.patch.object(harness.charm.upgrade, "log_rollback_instructions") + mocker.patch.object(harness.charm.upgrade, "set_unit_failed") + + harness.charm.on.upgrade_charm.emit() + + harness.charm.upgrade.log_rollback_instructions.assert_called_once() + harness.charm.upgrade.set_unit_failed.assert_called_once() + + +def test_upgrade_charm_runs_checks_on_leader(harness, mocker): + harness.charm.upgrade = GandalfUpgrade( + charm=harness.charm, dependency_model=GandalfModel(**GANDALF_DEPS), substrate="k8s" + ) + harness.add_relation("upgrade", "gandalf") + harness.charm.upgrade.peer_relation.data[harness.charm.unit].update({"state": "idle"}) + harness.charm.upgrade.upgrade_stack = [0] + + mocker.patch.object(harness.charm.upgrade, "_upgrade_supported_check") + harness.charm.on.upgrade_charm.emit() + + harness.charm.upgrade._upgrade_supported_check.assert_not_called() + + harness.set_leader(True) + harness.charm.on.upgrade_charm.emit() + + harness.charm.upgrade._upgrade_supported_check.assert_called_once() + + +def test_upgrade_charm_sets_ready(harness, mocker): + harness.charm.upgrade = GandalfUpgrade( + charm=harness.charm, dependency_model=GandalfModel(**GANDALF_DEPS), substrate="k8s" + ) + harness.add_relation("upgrade", "gandalf") + harness.charm.upgrade.peer_relation.data[harness.charm.unit].update({"state": "idle"}) + harness.charm.upgrade.upgrade_stack = [0] + harness.set_leader(True) + + mocker.patch.object(harness.charm.upgrade, "_upgrade_supported_check") + harness.charm.on.upgrade_charm.emit() + + assert harness.charm.upgrade.state == "ready" + + +def test_upgrade_changed_fails_unit_if_any_failed(harness, mocker): + harness.charm.upgrade = GandalfUpgrade( + charm=harness.charm, dependency_model=GandalfModel(**GANDALF_DEPS), substrate="k8s" + ) + harness.add_relation("upgrade", "gandalf") + harness.charm.upgrade.upgrade_stack = [0] + harness.charm.upgrade.peer_relation.data[harness.charm.unit].update({"state": "completed"}) + harness.add_relation_unit(harness.charm.upgrade.peer_relation.id, "gandalf/1") + + mocker.patch.object(harness.charm.upgrade, "log_rollback_instructions") + mocker.patch.object(harness.charm.upgrade, "set_unit_failed") + + harness.update_relation_data( + harness.charm.upgrade.peer_relation.id, "gandalf/1", {"state": "failed"} + ) + + harness.charm.upgrade.log_rollback_instructions.assert_called_once() + harness.charm.upgrade.set_unit_failed.assert_called_once() + + +def test_upgrade_changed_sets_idle_if_all_completed(harness): + harness.charm.upgrade = GandalfUpgrade( + charm=harness.charm, dependency_model=GandalfModel(**GANDALF_DEPS), substrate="k8s" + ) + harness.add_relation("upgrade", "gandalf") + harness.charm.upgrade.upgrade_stack = [] + harness.charm.upgrade.peer_relation.data[harness.charm.unit].update({"state": "completed"}) + harness.add_relation_unit(harness.charm.upgrade.peer_relation.id, "gandalf/1") + harness.update_relation_data( + harness.charm.upgrade.peer_relation.id, "gandalf/1", {"state": "completed"} + ) + + assert harness.charm.upgrade.state == "idle" + + +def test_upgrade_changed_sets_idle_if_some_completed_idle(harness): + harness.charm.upgrade = GandalfUpgrade( + charm=harness.charm, dependency_model=GandalfModel(**GANDALF_DEPS), substrate="k8s" + ) + harness.add_relation("upgrade", "gandalf") + harness.charm.upgrade.upgrade_stack = [] + harness.charm.upgrade.peer_relation.data[harness.charm.unit].update({"state": "completed"}) + harness.add_relation_unit(harness.charm.upgrade.peer_relation.id, "gandalf/1") + harness.update_relation_data( + harness.charm.upgrade.peer_relation.id, "gandalf/1", {"state": "idle"} + ) + + assert harness.charm.upgrade.state == "idle" + + +def test_upgrade_changed_does_not_recurse_if_called_all_idle(harness, mocker): + harness.charm.upgrade = GandalfUpgrade( + charm=harness.charm, dependency_model=GandalfModel(**GANDALF_DEPS), substrate="k8s" + ) + harness.add_relation("upgrade", "gandalf") + harness.charm.upgrade.upgrade_stack = [] + harness.charm.upgrade.peer_relation.data[harness.charm.unit].update({"state": "idle"}) + harness.add_relation_unit(harness.charm.upgrade.peer_relation.id, "gandalf/1") + harness.set_leader(True) + + mocker.patch.object(harness.charm.upgrade, "on_upgrade_changed") + harness.update_relation_data( + harness.charm.upgrade.peer_relation.id, "gandalf/1", {"state": "idle"} + ) + + harness.charm.upgrade.on_upgrade_changed.assert_called_once() + + +@pytest.mark.parametrize( + "pre_state,stack,call_count,post_state", + [ + ("idle", [0, 1], 0, "idle"), + ("idle", [1, 0], 0, "idle"), + ("ready", [0, 1], 0, "ready"), + ("ready", [1, 0], 1, "upgrading"), + ], +) +def test_upgrade_changed_emits_upgrade_granted_only_if_top_of_stack( + harness, mocker, pre_state, stack, call_count, post_state +): + harness.charm.upgrade = GandalfUpgrade( + charm=harness.charm, dependency_model=GandalfModel(**GANDALF_DEPS), substrate="k8s" + ) + + with harness.hooks_disabled(): + harness.add_relation("upgrade", "gandalf") + harness.charm.upgrade.upgrade_stack = stack + harness.add_relation_unit(harness.charm.upgrade.peer_relation.id, "gandalf/1") + harness.charm.upgrade.peer_relation.data[harness.charm.unit].update({"state": pre_state}) + + upgrade_granted_spy = mocker.spy(harness.charm.upgrade, "_on_upgrade_granted") + + harness.update_relation_data( + harness.charm.upgrade.peer_relation.id, "gandalf/1", {"state": "ready"} + ) + + assert upgrade_granted_spy.call_count == call_count + assert harness.charm.upgrade.state == post_state + + +def test_upgrade_changed_recurses_on_leader_and_clears_stack(harness, mocker): + harness.charm.upgrade = GandalfUpgrade( + charm=harness.charm, dependency_model=GandalfModel(**GANDALF_DEPS), substrate="k8s" + ) + harness.add_relation("upgrade", "gandalf") + harness.charm.upgrade.upgrade_stack = [0, 1] + harness.charm.upgrade.peer_relation.data[harness.charm.unit].update({"state": "ready"}) + harness.add_relation_unit(harness.charm.upgrade.peer_relation.id, "gandalf/1") + harness.set_leader(True) + + upgrade_changed_spy = mocker.spy(harness.charm.upgrade, "on_upgrade_changed") + + harness.update_relation_data( + harness.charm.upgrade.peer_relation.id, "gandalf/1", {"state": "completed"} + ) + + # once for top of stack 1, once for leader 0 + assert upgrade_changed_spy.call_count == 2 + assert harness.charm.upgrade.upgrade_stack == [] + assert json.loads( + harness.charm.upgrade.peer_relation.data[harness.charm.app].get("upgrade-stack", "") + ) == [0] + + +def test_upgrade_changed_does_not_recurse_or_change_stack_non_leader(harness, mocker): + harness.charm.upgrade = GandalfUpgrade( + charm=harness.charm, dependency_model=GandalfModel(**GANDALF_DEPS), substrate="k8s" + ) + harness.add_relation("upgrade", "gandalf") + harness.charm.upgrade.upgrade_stack = [0, 1] + harness.charm.upgrade.peer_relation.data[harness.charm.unit].update({"state": "ready"}) + harness.add_relation_unit(harness.charm.upgrade.peer_relation.id, "gandalf/1") + + upgrade_changed_spy = mocker.spy(harness.charm.upgrade, "on_upgrade_changed") + + harness.update_relation_data( + harness.charm.upgrade.peer_relation.id, "gandalf/1", {"state": "completed"} + ) + + # once for top of stack 1 + assert upgrade_changed_spy.call_count == 1 + assert json.loads( + harness.charm.upgrade.peer_relation.data[harness.charm.app].get("upgrade-stack", "") + ) == [0, 1] diff --git a/tox.ini b/tox.ini index d4fbe6eb..32e7a30f 100644 --- a/tox.ini +++ b/tox.ini @@ -65,6 +65,7 @@ deps = parameterized psycopg[binary] pytest + pytest-mock coverage[toml] -r {tox_root}/requirements.txt commands =