From b5484d287497208e8aa1cdc5dc187589fcf56f3e Mon Sep 17 00:00:00 2001 From: Robert Gildein Date: Tue, 26 Mar 2024 18:28:13 +0100 Subject: [PATCH 01/11] add base structure for test from yaml --- setup.cfg | 1 + tests/unit/apps_planning/test_apps_plan.py | 33 +++++ tests/unit/conftest.py | 29 ++-- tests/unit/sample_plans/base.yaml | 147 +++++++++++++++++++++ tests/unit/utils.py | 77 ++++++++++- 5 files changed, 271 insertions(+), 16 deletions(-) create mode 100644 tests/unit/apps_planning/test_apps_plan.py create mode 100644 tests/unit/sample_plans/base.yaml diff --git a/setup.cfg b/setup.cfg index 8f7c46aa..a6c14a94 100644 --- a/setup.cfg +++ b/setup.cfg @@ -52,6 +52,7 @@ unittests = pytest-cov pytest-mock pytest-asyncio + pytest-subtests aiounittest gevent diff --git a/tests/unit/apps_planning/test_apps_plan.py b/tests/unit/apps_planning/test_apps_plan.py new file mode 100644 index 00000000..01cfe815 --- /dev/null +++ b/tests/unit/apps_planning/test_apps_plan.py @@ -0,0 +1,33 @@ +# Copyright 2023 Canonical Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Test all sample plans.""" +from unittest.mock import patch + +import pytest + +from cou.commands import CLIargs +from cou.steps.plan import generate_plan + + +@pytest.mark.asyncio +@patch("cou.steps.plan.filter_hypervisors_machines") +async def test_sample_plans_no_inputs(_, subtests, sample_plans): + """Testing all sample plans.""" + args = CLIargs("plan", auto_approve=True) + + for analysis_create_coro, exp_plan, file in sample_plans: + with subtests.test(msg=file.name): + analysis_results = await analysis_create_coro + plan = await generate_plan(analysis_results, args) + assert str(plan) == exp_plan diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 9f81902d..c342e273 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -13,26 +13,14 @@ # limitations under the License. from pathlib import Path +from typing import Generator from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch import pytest -from juju.client.client import FullStatus from cou.commands import CLIargs - - -def get_status(): - """Help function to load Juju status from json file.""" - current_path = Path(__file__).parent.resolve() - with open(current_path / "jujustatus.json", "r") as file: - status = file.read().rstrip() - - return FullStatus.from_json(status) - - -async def get_charm_name(value: str): - """Help function to get charm name.""" - return value +from cou.steps.analyze import Analysis +from tests.unit.utils import get_charm_name, get_sample_plan, get_status @pytest.fixture @@ -71,3 +59,14 @@ def cli_args() -> MagicMock: """ # spec_set needs an instantiated class to be strict with the fields. return MagicMock(spec_set=CLIargs(command="plan"))() + + +@pytest.fixture +def sample_plans(model) -> Generator[tuple[Analysis, str], None, None]: + """Fixture that returns all sample plans in a directory.""" + directory = Path(__file__).parent / "sample_plans" + sample_plans = [ + get_sample_plan(model, sample_file) for sample_file in directory.glob("*.yaml") + ] + model.get_applications = AsyncMock(side_effect=[apps for apps, _, _ in sample_plans]) + return [(Analysis.create(model), exp_plan, file) for _, exp_plan, file in sample_plans] diff --git a/tests/unit/sample_plans/base.yaml b/tests/unit/sample_plans/base.yaml new file mode 100644 index 00000000..c61960b2 --- /dev/null +++ b/tests/unit/sample_plans/base.yaml @@ -0,0 +1,147 @@ +plan: | + Upgrade cloud from 'ussuri' to 'victoria' + Verify that all OpenStack applications are in idle state + Back up MySQL databases + Control Plane principal(s) upgrade plan + Upgrade plan for 'keystone' to 'victoria' + Upgrade software packages of 'keystone' from the current APT repositories + Upgrade software packages on unit 'keystone/0' + Refresh 'keystone' to the latest revision of 'ussuri/stable' + Change charm config of 'keystone' 'action-managed-upgrade' to 'False' + Upgrade 'keystone' to the new channel: 'victoria/stable' + Change charm config of 'keystone' 'openstack-origin' to 'cloud:focal-victoria' + Wait for up to 1800s for model 'test_model' to reach the idle state + Verify that the workload of 'keystone' has been upgraded on units: keystone/0 + Control Plane subordinate(s) upgrade plan + Upgrade plan for 'keystone-ldap' to 'victoria' + Refresh 'keystone-ldap' to the latest revision of 'ussuri/stable' + Upgrade 'keystone-ldap' to the new channel: 'victoria/stable' + Upgrading all applications deployed on machines with hypervisor. + Upgrade plan for 'az-1' to 'victoria' + Upgrade software packages of 'nova-compute' from the current APT repositories + Upgrade software packages on unit 'nova-compute/0' + Refresh 'nova-compute' to the latest revision of 'ussuri/stable' + Change charm config of 'nova-compute' 'action-managed-upgrade' to 'True' + Upgrade 'nova-compute' to the new channel: 'victoria/stable' + Change charm config of 'nova-compute' 'source' to 'cloud:focal-victoria' + Upgrade plan for units: nova-compute/0 + Upgrade plan for unit 'nova-compute/0' + Disable nova-compute scheduler from unit: 'nova-compute/0' + Verify that unit 'nova-compute/0' has no VMs running + ├── Pause the unit: 'nova-compute/0' + ├── Upgrade the unit: 'nova-compute/0' + ├── Resume the unit: 'nova-compute/0' + Enable nova-compute scheduler from unit: 'nova-compute/0' + Wait for up to 1800s for model 'test_model' to reach the idle state + Verify that the workload of 'nova-compute' has been upgraded on units: nova-compute/0 + Remaining Data Plane principal(s) upgrade plan + Upgrade plan for 'ceph-osd' to 'victoria' + Verify that all 'nova-compute' units had been upgraded + Upgrade software packages of 'ceph-osd' from the current APT repositories + Upgrade software packages on unit 'ceph-osd/0' + Change charm config of 'ceph-osd' 'source' to 'cloud:focal-victoria' + Wait for up to 300s for app 'ceph-osd' to reach the idle state + Verify that the workload of 'ceph-osd' has been upgraded on units: ceph-osd/0 + Data Plane subordinate(s) upgrade plan + Upgrade plan for 'ovn-chassis' to 'victoria' + Refresh 'ovn-chassis' to the latest revision of '22.03/stable' + +applications: + keystone: + can_upgrade_to: ussuri/stable + charm: keystone + channel: ussuri/stable + config: + openstack-origin: + value: distro + action-managed-upgrade: + value: true + origin: ch + series: focal + subordinate_to: [] + workload_version: 17.0.1 + units: + keystone/0: + name: keystone/0 + machine: '0' + workload_version: 17.0.1 + os_version: ussuri + machines: + '0': + id: '0' + apps: !!python/tuple ['keystone-ldap'] + az: az-0 + + keystone-ldap: + can_upgrade_to: ussuri/stable + charm: keystone-ldap + channel: ussuri/stable + config: {} + origin: ch + series: focal + subordinate_to: + - keystone + workload_version: 17.0.1 + units: {} + machines: {} + + ceph-osd: + can_upgrade_to: octopus/stable + charm: ceph-osd + channel: octopus/stable + config: + source: + value: distro + origin: ch + series: focal + subordinate_to: [] + workload_version: 17.0.1 + units: + ceph-osd/0: + name: ceph-osd/0 + machine: '2' + workload_version: 17.0.1 + os_version: xena + machines: + '2': + id: '2' + apps: !!python/tuple [] + az: az-0 + + nova-compute: + can_upgrade_to: ussuri/stable + charm: nova-compute + channel: ussuri/stable + config: + source: + value: distro + action-managed-upgrade: + value: false + origin: ch + series: focal + subordinate_to: [] + workload_version: 21.0.0 + units: + nova-compute/0: + name: nova-compute/0 + machine: '1' + workload_version: 21.0.0 + os_version: ussuri + machines: + '1': + id: '1' + apps: !!python/tuple ['ovn-chassis'] + az: az-0 + + ovn-chassis: + can_upgrade_to: 22.03/stable + charm: ovn-chassis + channel: 22.03/stable + config: {} + origin: ch + series: focal + subordinate_to: + - nova-compute + workload_version: '22.3' + units: {} + machines: {} diff --git a/tests/unit/utils.py b/tests/unit/utils.py index 9513c91e..233a731c 100644 --- a/tests/unit/utils.py +++ b/tests/unit/utils.py @@ -12,11 +12,16 @@ # See the License for the specific language governing permissions and # limitations under the License. """Module to provide helper for writing unit tests.""" +from pathlib import Path from textwrap import dedent from unittest.mock import MagicMock +import yaml +from juju.client.client import FullStatus + +from cou.apps.base import OpenStackApplication from cou.steps import BaseStep -from cou.utils.juju_utils import Machine +from cou.utils.juju_utils import Application, Machine, Model, Unit def assert_steps(step_1: BaseStep, step_2: BaseStep) -> None: @@ -38,3 +43,73 @@ def dedent_plan(plan: str) -> str: result = dedent(plan) result = result.replace(" ", "\t") # replace 4 spaces with tap return result + + +def get_status(): + """Help function to load Juju status from json file.""" + current_path = Path(__file__).parent.resolve() + with open(current_path / "jujustatus.json", "r") as file: + status = file.read().rstrip() + + return FullStatus.from_json(status) + + +async def get_charm_name(value: str): + """Help function to get charm name.""" + return value + + +def get_sample_plan( + model: Model, source: Path +) -> tuple[dict[str, OpenStackApplication], str, Path]: + """Help function to get dict of Applications and expected upgrade plan from file. + + This function can load applications from yaml format, where each app is string representation + of OpenStackApplication (str(OpenStackApplication), see OpenStackApplication.__str__). + + applications: + : + model_name: ... + can_upgrade_to: ... + ... + : + model_name: ... + can_upgrade_to: ... + ... + plan: | + ... + """ + with open(source, "r") as file: + data = yaml.load(file, Loader=yaml.Loader) + + # Note(rgildein): We need to get machines first, since they are used in Unit object. + machines = { + machine_id: Machine(machine["id"], machine["apps"], machine["az"]) + for app_data in data["applications"].values() + for machine_id, machine in app_data["machines"].items() + } + + return ( + { + name: Application( + name=name, + can_upgrade_to=app_data["can_upgrade_to"], + charm=app_data["charm"], + channel=app_data["channel"], + config=app_data["config"], + machines={machine_id: machines[machine_id] for machine_id in app_data["machines"]}, + model=model, + origin=app_data["origin"], + series=app_data["series"], + subordinate_to=app_data["subordinate_to"], + units={ + name: Unit(name, machines[unit["machine"]], unit["workload_version"]) + for name, unit in app_data["units"].items() + }, + workload_version=app_data["workload_version"], + ) + for name, app_data in data["applications"].items() + }, + dedent_plan(data["plan"]), + source, + ) From 78cc04483f27c6cfa6b73713ac6a6ebef6fa6c6e Mon Sep 17 00:00:00 2001 From: Robert Gildein Date: Wed, 27 Mar 2024 14:39:26 +0100 Subject: [PATCH 02/11] change from fixture to single test per file --- cou/utils/nova_compute.py | 2 +- setup.cfg | 1 - tests/unit/apps_planning/test_apps_plan.py | 15 +++--- tests/unit/conftest.py | 28 ++++++----- tests/unit/sample_plans/base.yaml | 26 ++++++---- tests/unit/utils.py | 58 +++++++++++----------- 6 files changed, 70 insertions(+), 60 deletions(-) diff --git a/cou/utils/nova_compute.py b/cou/utils/nova_compute.py index abf0b7f8..faf433a4 100644 --- a/cou/utils/nova_compute.py +++ b/cou/utils/nova_compute.py @@ -26,7 +26,7 @@ async def get_empty_hypervisors(units: list[Unit], model: Model) -> list[Machine]: """Get the empty hypervisors in the model. - :param units: all nova-compute units. + :param units: All nova-compute units. :type units: list[Unit] :param model: Model object :type model: Model diff --git a/setup.cfg b/setup.cfg index a6c14a94..8f7c46aa 100644 --- a/setup.cfg +++ b/setup.cfg @@ -52,7 +52,6 @@ unittests = pytest-cov pytest-mock pytest-asyncio - pytest-subtests aiounittest gevent diff --git a/tests/unit/apps_planning/test_apps_plan.py b/tests/unit/apps_planning/test_apps_plan.py index 01cfe815..9531fe58 100644 --- a/tests/unit/apps_planning/test_apps_plan.py +++ b/tests/unit/apps_planning/test_apps_plan.py @@ -17,17 +17,18 @@ import pytest from cou.commands import CLIargs +from cou.steps.analyze import Analysis from cou.steps.plan import generate_plan @pytest.mark.asyncio -@patch("cou.steps.plan.filter_hypervisors_machines") -async def test_sample_plans_no_inputs(_, subtests, sample_plans): +@patch("cou.utils.nova_compute.get_instance_count", return_value=0) +async def test_base_plan(_, model, sample_plans): """Testing all sample plans.""" args = CLIargs("plan", auto_approve=True) + model, exp_plan = sample_plans["base.yaml"] - for analysis_create_coro, exp_plan, file in sample_plans: - with subtests.test(msg=file.name): - analysis_results = await analysis_create_coro - plan = await generate_plan(analysis_results, args) - assert str(plan) == exp_plan + analysis_results = await Analysis.create(model) + plan = await generate_plan(analysis_results, args) + + assert str(plan) == exp_plan diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index c342e273..3ae9672d 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -13,23 +13,21 @@ # limitations under the License. from pathlib import Path -from typing import Generator from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch import pytest from cou.commands import CLIargs -from cou.steps.analyze import Analysis +from cou.utils.juju_utils import Model from tests.unit.utils import get_charm_name, get_sample_plan, get_status @pytest.fixture -def model(): +def model() -> AsyncMock: """Define test Model object.""" model_name = "test_model" - from cou.utils import juju_utils - model = AsyncMock(spec_set=juju_utils.Model) + model = AsyncMock(spec_set=Model) type(model).name = PropertyMock(return_value=model_name) model.run_on_unit = AsyncMock() model.run_action = AsyncMock() @@ -61,12 +59,16 @@ def cli_args() -> MagicMock: return MagicMock(spec_set=CLIargs(command="plan"))() -@pytest.fixture -def sample_plans(model) -> Generator[tuple[Analysis, str], None, None]: - """Fixture that returns all sample plans in a directory.""" +@pytest.fixture(scope="session") +def sample_plans() -> dict[str, tuple[Model, str]]: + """Fixture that returns all sample plans in a directory. + + This fixture returns a dictionary with filename as key and value as a + cou.utils.juju_utils.Model object whose get_applications function returns the applications + from the file and the expected plan. + """ directory = Path(__file__).parent / "sample_plans" - sample_plans = [ - get_sample_plan(model, sample_file) for sample_file in directory.glob("*.yaml") - ] - model.get_applications = AsyncMock(side_effect=[apps for apps, _, _ in sample_plans]) - return [(Analysis.create(model), exp_plan, file) for _, exp_plan, file in sample_plans] + + yield { + sample_file.name: get_sample_plan(sample_file) for sample_file in directory.glob("*.yaml") + } diff --git a/tests/unit/sample_plans/base.yaml b/tests/unit/sample_plans/base.yaml index c61960b2..970730eb 100644 --- a/tests/unit/sample_plans/base.yaml +++ b/tests/unit/sample_plans/base.yaml @@ -10,14 +10,14 @@ plan: | Change charm config of 'keystone' 'action-managed-upgrade' to 'False' Upgrade 'keystone' to the new channel: 'victoria/stable' Change charm config of 'keystone' 'openstack-origin' to 'cloud:focal-victoria' - Wait for up to 1800s for model 'test_model' to reach the idle state + Wait for up to 1800s for model 'base' to reach the idle state Verify that the workload of 'keystone' has been upgraded on units: keystone/0 Control Plane subordinate(s) upgrade plan Upgrade plan for 'keystone-ldap' to 'victoria' Refresh 'keystone-ldap' to the latest revision of 'ussuri/stable' Upgrade 'keystone-ldap' to the new channel: 'victoria/stable' Upgrading all applications deployed on machines with hypervisor. - Upgrade plan for 'az-1' to 'victoria' + Upgrade plan for 'az-0' to 'victoria' Upgrade software packages of 'nova-compute' from the current APT repositories Upgrade software packages on unit 'nova-compute/0' Refresh 'nova-compute' to the latest revision of 'ussuri/stable' @@ -32,11 +32,11 @@ plan: | ├── Upgrade the unit: 'nova-compute/0' ├── Resume the unit: 'nova-compute/0' Enable nova-compute scheduler from unit: 'nova-compute/0' - Wait for up to 1800s for model 'test_model' to reach the idle state + Wait for up to 1800s for model 'base' to reach the idle state Verify that the workload of 'nova-compute' has been upgraded on units: nova-compute/0 Remaining Data Plane principal(s) upgrade plan Upgrade plan for 'ceph-osd' to 'victoria' - Verify that all 'nova-compute' units had been upgraded + Verify that all 'nova-compute' units has been upgraded Upgrade software packages of 'ceph-osd' from the current APT repositories Upgrade software packages on unit 'ceph-osd/0' Change charm config of 'ceph-osd' 'source' to 'cloud:focal-victoria' @@ -69,7 +69,7 @@ applications: machines: '0': id: '0' - apps: !!python/tuple ['keystone-ldap'] + apps: !!python/tuple ['keystone', 'keystone-ldap'] az: az-0 keystone-ldap: @@ -83,7 +83,11 @@ applications: - keystone workload_version: 17.0.1 units: {} - machines: {} + machines: + '0': + id: '0' + apps: !!python/tuple ['keystone', 'keystone-ldap'] + az: az-0 ceph-osd: can_upgrade_to: octopus/stable @@ -105,7 +109,7 @@ applications: machines: '2': id: '2' - apps: !!python/tuple [] + apps: !!python/tuple ['ceph-osd'] az: az-0 nova-compute: @@ -130,7 +134,7 @@ applications: machines: '1': id: '1' - apps: !!python/tuple ['ovn-chassis'] + apps: !!python/tuple ['nova-compute', 'ovn-chassis'] az: az-0 ovn-chassis: @@ -144,4 +148,8 @@ applications: - nova-compute workload_version: '22.3' units: {} - machines: {} + machines: + '1': + id: '1' + apps: !!python/tuple ['nova-compute', 'ovn-chassis'] + az: az-0 diff --git a/tests/unit/utils.py b/tests/unit/utils.py index 233a731c..d076953b 100644 --- a/tests/unit/utils.py +++ b/tests/unit/utils.py @@ -14,12 +14,11 @@ """Module to provide helper for writing unit tests.""" from pathlib import Path from textwrap import dedent -from unittest.mock import MagicMock +from unittest.mock import AsyncMock, MagicMock, PropertyMock import yaml from juju.client.client import FullStatus -from cou.apps.base import OpenStackApplication from cou.steps import BaseStep from cou.utils.juju_utils import Application, Machine, Model, Unit @@ -59,9 +58,7 @@ async def get_charm_name(value: str): return value -def get_sample_plan( - model: Model, source: Path -) -> tuple[dict[str, OpenStackApplication], str, Path]: +def get_sample_plan(source: Path) -> tuple[Model, str]: """Help function to get dict of Applications and expected upgrade plan from file. This function can load applications from yaml format, where each app is string representation @@ -82,34 +79,37 @@ def get_sample_plan( with open(source, "r") as file: data = yaml.load(file, Loader=yaml.Loader) + model = AsyncMock(spec_set=Model) + # Note(rgildein): We need to get machines first, since they are used in Unit object. machines = { machine_id: Machine(machine["id"], machine["apps"], machine["az"]) for app_data in data["applications"].values() for machine_id, machine in app_data["machines"].items() } + applications = { + name: Application( + name=name, + can_upgrade_to=app_data["can_upgrade_to"], + charm=app_data["charm"], + channel=app_data["channel"], + config=app_data["config"], + machines={machine_id: machines[machine_id] for machine_id in app_data["machines"]}, + model=model, + origin=app_data["origin"], + series=app_data["series"], + subordinate_to=app_data["subordinate_to"], + units={ + name: Unit(name, machines[unit["machine"]], unit["workload_version"]) + for name, unit in app_data["units"].items() + }, + workload_version=app_data["workload_version"], + ) + for name, app_data in data["applications"].items() + } + + type(model).name = PropertyMock(return_value=source.stem) + # model._get_machines = AsyncMock(return_value=machines) + model.get_applications = AsyncMock(return_value=applications) - return ( - { - name: Application( - name=name, - can_upgrade_to=app_data["can_upgrade_to"], - charm=app_data["charm"], - channel=app_data["channel"], - config=app_data["config"], - machines={machine_id: machines[machine_id] for machine_id in app_data["machines"]}, - model=model, - origin=app_data["origin"], - series=app_data["series"], - subordinate_to=app_data["subordinate_to"], - units={ - name: Unit(name, machines[unit["machine"]], unit["workload_version"]) - for name, unit in app_data["units"].items() - }, - workload_version=app_data["workload_version"], - ) - for name, app_data in data["applications"].items() - }, - dedent_plan(data["plan"]), - source, - ) + return model, dedent_plan(data["plan"]) From 1eb84a4eb4d2d7cff40eba9a250effd7c9c4d5ea Mon Sep 17 00:00:00 2001 From: Robert Gildein Date: Thu, 28 Mar 2024 13:42:12 +0100 Subject: [PATCH 03/11] Add mocked-plans tox environment Add mocked-plans tox environment and job in GitHub workflows. I also remove usage of boots-actions, since it's old and not maintained. --- .github/workflows/check.yaml | 56 ++++++++++++++++++++++++++++++------ pyproject.toml | 5 +--- tox.ini | 4 +++ 3 files changed, 52 insertions(+), 13 deletions(-) diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index 4dbace8e..8705fb7f 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -17,15 +17,53 @@ concurrency: jobs: lint-unit: - name: Lint checkers and unit tests - uses: canonical/bootstack-actions/.github/workflows/lint-unit.yaml@v2 - strategy: - fail-fast: false - matrix: - python-version: ['3.10'] - with: - python-version: ${{ matrix.python-version }} - tox-version: '<4' + name: Lint checkers and Unit tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + submodules: true + - name: Set up Python 3.10 + uses: actions/setup-python@v4 + with: + python-version: '3.10' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install tox + - name: Run lint checkers + run: tox -e lint + - name: Run unit tests + run: tox -e unit + - name: Save PR number to file + run: echo ${{ github.event.number }} > PR_NUMBER.txt + - name: Archive PR number + uses: actions/upload-artifact@v3 + with: + name: PR_NUMBER + path: PR_NUMBER.txt + - name: Archive code coverage results + uses: actions/upload-artifact@v3 + with: + name: coverage-report + path: ./tests/unit/report/coverage.xml + + mocked-plans: + name: Mocked plans tests + needs: lint-unit + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up Python 3.10 + uses: actions/setup-python@v4 + with: + python-version: '3.10' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install tox + - name: Run unit tests + run: tox -e mocked-plans snap-build: name: Build snap package diff --git a/pyproject.toml b/pyproject.toml index c556d392..7d91072e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,3 @@ -# This is a template `pyproject.toml` file for snaps -# This file is managed by bootstack-charms-spec and should not be modified -# within individual snap repos. https://launchpad.net/bootstack-charms-spec - [tool.flake8] ignore = ["C901", "D100", "D101", "D102", "D103", "W503", "W504"] exclude = ['.eggs', '.git', '.tox', '.venv', '.build', 'build', 'report', 'docs'] @@ -106,6 +102,7 @@ quiet-level = 3 check-filenames = true [tool.pytest.ini_options] +addopts = "--ignore=tests/unit/apps_planning" filterwarnings = [ "ignore::RuntimeWarning", ] diff --git a/tox.ini b/tox.ini index 5dce29d0..31e55346 100644 --- a/tox.ini +++ b/tox.ini @@ -53,6 +53,10 @@ deps = .[unittests] commands = pytest {toxinidir}/tests/unit \ {posargs:-v --cov --cov-report=term-missing --cov-report=html --cov-report=xml} +[testenv:mocked-plans] +deps = .[unittests] +commands = pytest {toxinidir}/tests/unit/apps_planning {posargs:-s -vv} + [testenv:func] changedir = {toxinidir}/tests/functional deps = .[functests] From 7998cf5b9711624636af3192b162a9cfc92bb115 Mon Sep 17 00:00:00 2001 From: Robert Gildein Date: Thu, 28 Mar 2024 13:46:46 +0100 Subject: [PATCH 04/11] update to newer GitHub actions --- .github/workflows/check.yaml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index 8705fb7f..29058a2c 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -20,11 +20,11 @@ jobs: name: Lint checkers and Unit tests runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: submodules: true - name: Set up Python 3.10 - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: '3.10' - name: Install dependencies @@ -38,12 +38,12 @@ jobs: - name: Save PR number to file run: echo ${{ github.event.number }} > PR_NUMBER.txt - name: Archive PR number - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: PR_NUMBER path: PR_NUMBER.txt - name: Archive code coverage results - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: coverage-report path: ./tests/unit/report/coverage.xml @@ -53,9 +53,9 @@ jobs: needs: lint-unit runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python 3.10 - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: '3.10' - name: Install dependencies @@ -75,7 +75,7 @@ jobs: with: submodules: true - name: Setup Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: '3.10' - name: Install tox @@ -91,7 +91,7 @@ jobs: - name: Build snap run: make build - name: Upload the built snap as an artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: SNAP_FILE path: charmed-openstack-upgrader.snap @@ -106,7 +106,7 @@ jobs: with: submodules: true - name: Setup Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: '3.10' - name: Setup Juju 2.9/stable environment @@ -121,7 +121,7 @@ jobs: python -m pip install --upgrade pip python -m pip install tox - name: Download snap file artifact - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: SNAP_FILE - name: Run func tests From 332d7dba274a20229fc3b4f73a9a6d6374ec00df Mon Sep 17 00:00:00 2001 From: Robert Gildein Date: Tue, 2 Apr 2024 15:26:21 +0200 Subject: [PATCH 05/11] move to separate directory --- pyproject.toml | 1 - tests/unit/apps_planning/test_apps_plan.py | 34 ----- tests/unit/conftest.py | 18 +-- tests/unit/sample_plans/base.yaml | 155 --------------------- tests/unit/utils.py | 62 +-------- tox.ini | 4 +- 6 files changed, 5 insertions(+), 269 deletions(-) delete mode 100644 tests/unit/apps_planning/test_apps_plan.py delete mode 100644 tests/unit/sample_plans/base.yaml diff --git a/pyproject.toml b/pyproject.toml index 7d91072e..2c446d4c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -102,7 +102,6 @@ quiet-level = 3 check-filenames = true [tool.pytest.ini_options] -addopts = "--ignore=tests/unit/apps_planning" filterwarnings = [ "ignore::RuntimeWarning", ] diff --git a/tests/unit/apps_planning/test_apps_plan.py b/tests/unit/apps_planning/test_apps_plan.py deleted file mode 100644 index 9531fe58..00000000 --- a/tests/unit/apps_planning/test_apps_plan.py +++ /dev/null @@ -1,34 +0,0 @@ -# Copyright 2023 Canonical Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Test all sample plans.""" -from unittest.mock import patch - -import pytest - -from cou.commands import CLIargs -from cou.steps.analyze import Analysis -from cou.steps.plan import generate_plan - - -@pytest.mark.asyncio -@patch("cou.utils.nova_compute.get_instance_count", return_value=0) -async def test_base_plan(_, model, sample_plans): - """Testing all sample plans.""" - args = CLIargs("plan", auto_approve=True) - model, exp_plan = sample_plans["base.yaml"] - - analysis_results = await Analysis.create(model) - plan = await generate_plan(analysis_results, args) - - assert str(plan) == exp_plan diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 3ae9672d..45251830 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -12,14 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -from pathlib import Path from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch import pytest from cou.commands import CLIargs from cou.utils.juju_utils import Model -from tests.unit.utils import get_charm_name, get_sample_plan, get_status +from tests.unit.utils import get_charm_name, get_status @pytest.fixture @@ -57,18 +56,3 @@ def cli_args() -> MagicMock: """ # spec_set needs an instantiated class to be strict with the fields. return MagicMock(spec_set=CLIargs(command="plan"))() - - -@pytest.fixture(scope="session") -def sample_plans() -> dict[str, tuple[Model, str]]: - """Fixture that returns all sample plans in a directory. - - This fixture returns a dictionary with filename as key and value as a - cou.utils.juju_utils.Model object whose get_applications function returns the applications - from the file and the expected plan. - """ - directory = Path(__file__).parent / "sample_plans" - - yield { - sample_file.name: get_sample_plan(sample_file) for sample_file in directory.glob("*.yaml") - } diff --git a/tests/unit/sample_plans/base.yaml b/tests/unit/sample_plans/base.yaml deleted file mode 100644 index 970730eb..00000000 --- a/tests/unit/sample_plans/base.yaml +++ /dev/null @@ -1,155 +0,0 @@ -plan: | - Upgrade cloud from 'ussuri' to 'victoria' - Verify that all OpenStack applications are in idle state - Back up MySQL databases - Control Plane principal(s) upgrade plan - Upgrade plan for 'keystone' to 'victoria' - Upgrade software packages of 'keystone' from the current APT repositories - Upgrade software packages on unit 'keystone/0' - Refresh 'keystone' to the latest revision of 'ussuri/stable' - Change charm config of 'keystone' 'action-managed-upgrade' to 'False' - Upgrade 'keystone' to the new channel: 'victoria/stable' - Change charm config of 'keystone' 'openstack-origin' to 'cloud:focal-victoria' - Wait for up to 1800s for model 'base' to reach the idle state - Verify that the workload of 'keystone' has been upgraded on units: keystone/0 - Control Plane subordinate(s) upgrade plan - Upgrade plan for 'keystone-ldap' to 'victoria' - Refresh 'keystone-ldap' to the latest revision of 'ussuri/stable' - Upgrade 'keystone-ldap' to the new channel: 'victoria/stable' - Upgrading all applications deployed on machines with hypervisor. - Upgrade plan for 'az-0' to 'victoria' - Upgrade software packages of 'nova-compute' from the current APT repositories - Upgrade software packages on unit 'nova-compute/0' - Refresh 'nova-compute' to the latest revision of 'ussuri/stable' - Change charm config of 'nova-compute' 'action-managed-upgrade' to 'True' - Upgrade 'nova-compute' to the new channel: 'victoria/stable' - Change charm config of 'nova-compute' 'source' to 'cloud:focal-victoria' - Upgrade plan for units: nova-compute/0 - Upgrade plan for unit 'nova-compute/0' - Disable nova-compute scheduler from unit: 'nova-compute/0' - Verify that unit 'nova-compute/0' has no VMs running - ├── Pause the unit: 'nova-compute/0' - ├── Upgrade the unit: 'nova-compute/0' - ├── Resume the unit: 'nova-compute/0' - Enable nova-compute scheduler from unit: 'nova-compute/0' - Wait for up to 1800s for model 'base' to reach the idle state - Verify that the workload of 'nova-compute' has been upgraded on units: nova-compute/0 - Remaining Data Plane principal(s) upgrade plan - Upgrade plan for 'ceph-osd' to 'victoria' - Verify that all 'nova-compute' units has been upgraded - Upgrade software packages of 'ceph-osd' from the current APT repositories - Upgrade software packages on unit 'ceph-osd/0' - Change charm config of 'ceph-osd' 'source' to 'cloud:focal-victoria' - Wait for up to 300s for app 'ceph-osd' to reach the idle state - Verify that the workload of 'ceph-osd' has been upgraded on units: ceph-osd/0 - Data Plane subordinate(s) upgrade plan - Upgrade plan for 'ovn-chassis' to 'victoria' - Refresh 'ovn-chassis' to the latest revision of '22.03/stable' - -applications: - keystone: - can_upgrade_to: ussuri/stable - charm: keystone - channel: ussuri/stable - config: - openstack-origin: - value: distro - action-managed-upgrade: - value: true - origin: ch - series: focal - subordinate_to: [] - workload_version: 17.0.1 - units: - keystone/0: - name: keystone/0 - machine: '0' - workload_version: 17.0.1 - os_version: ussuri - machines: - '0': - id: '0' - apps: !!python/tuple ['keystone', 'keystone-ldap'] - az: az-0 - - keystone-ldap: - can_upgrade_to: ussuri/stable - charm: keystone-ldap - channel: ussuri/stable - config: {} - origin: ch - series: focal - subordinate_to: - - keystone - workload_version: 17.0.1 - units: {} - machines: - '0': - id: '0' - apps: !!python/tuple ['keystone', 'keystone-ldap'] - az: az-0 - - ceph-osd: - can_upgrade_to: octopus/stable - charm: ceph-osd - channel: octopus/stable - config: - source: - value: distro - origin: ch - series: focal - subordinate_to: [] - workload_version: 17.0.1 - units: - ceph-osd/0: - name: ceph-osd/0 - machine: '2' - workload_version: 17.0.1 - os_version: xena - machines: - '2': - id: '2' - apps: !!python/tuple ['ceph-osd'] - az: az-0 - - nova-compute: - can_upgrade_to: ussuri/stable - charm: nova-compute - channel: ussuri/stable - config: - source: - value: distro - action-managed-upgrade: - value: false - origin: ch - series: focal - subordinate_to: [] - workload_version: 21.0.0 - units: - nova-compute/0: - name: nova-compute/0 - machine: '1' - workload_version: 21.0.0 - os_version: ussuri - machines: - '1': - id: '1' - apps: !!python/tuple ['nova-compute', 'ovn-chassis'] - az: az-0 - - ovn-chassis: - can_upgrade_to: 22.03/stable - charm: ovn-chassis - channel: 22.03/stable - config: {} - origin: ch - series: focal - subordinate_to: - - nova-compute - workload_version: '22.3' - units: {} - machines: - '1': - id: '1' - apps: !!python/tuple ['nova-compute', 'ovn-chassis'] - az: az-0 diff --git a/tests/unit/utils.py b/tests/unit/utils.py index d076953b..f86746fe 100644 --- a/tests/unit/utils.py +++ b/tests/unit/utils.py @@ -14,13 +14,12 @@ """Module to provide helper for writing unit tests.""" from pathlib import Path from textwrap import dedent -from unittest.mock import AsyncMock, MagicMock, PropertyMock +from unittest.mock import MagicMock -import yaml from juju.client.client import FullStatus from cou.steps import BaseStep -from cou.utils.juju_utils import Application, Machine, Model, Unit +from cou.utils.juju_utils import Machine def assert_steps(step_1: BaseStep, step_2: BaseStep) -> None: @@ -56,60 +55,3 @@ def get_status(): async def get_charm_name(value: str): """Help function to get charm name.""" return value - - -def get_sample_plan(source: Path) -> tuple[Model, str]: - """Help function to get dict of Applications and expected upgrade plan from file. - - This function can load applications from yaml format, where each app is string representation - of OpenStackApplication (str(OpenStackApplication), see OpenStackApplication.__str__). - - applications: - : - model_name: ... - can_upgrade_to: ... - ... - : - model_name: ... - can_upgrade_to: ... - ... - plan: | - ... - """ - with open(source, "r") as file: - data = yaml.load(file, Loader=yaml.Loader) - - model = AsyncMock(spec_set=Model) - - # Note(rgildein): We need to get machines first, since they are used in Unit object. - machines = { - machine_id: Machine(machine["id"], machine["apps"], machine["az"]) - for app_data in data["applications"].values() - for machine_id, machine in app_data["machines"].items() - } - applications = { - name: Application( - name=name, - can_upgrade_to=app_data["can_upgrade_to"], - charm=app_data["charm"], - channel=app_data["channel"], - config=app_data["config"], - machines={machine_id: machines[machine_id] for machine_id in app_data["machines"]}, - model=model, - origin=app_data["origin"], - series=app_data["series"], - subordinate_to=app_data["subordinate_to"], - units={ - name: Unit(name, machines[unit["machine"]], unit["workload_version"]) - for name, unit in app_data["units"].items() - }, - workload_version=app_data["workload_version"], - ) - for name, app_data in data["applications"].items() - } - - type(model).name = PropertyMock(return_value=source.stem) - # model._get_machines = AsyncMock(return_value=machines) - model.get_applications = AsyncMock(return_value=applications) - - return model, dedent_plan(data["plan"]) diff --git a/tox.ini b/tox.ini index 31e55346..77106a36 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ [tox] skipsdist=True -envlist = lint, unit, func +envlist = lint, unit, mocked-plans, func skip_missing_interpreters = True [testenv] @@ -55,7 +55,7 @@ commands = pytest {toxinidir}/tests/unit \ [testenv:mocked-plans] deps = .[unittests] -commands = pytest {toxinidir}/tests/unit/apps_planning {posargs:-s -vv} +commands = pytest {toxinidir}/tests/mocked_plans/ {posargs:-s -vv} [testenv:func] changedir = {toxinidir}/tests/functional From 8f20ed00fdcabda915858cd823d6450bb0e99d21 Mon Sep 17 00:00:00 2001 From: Robert Gildein Date: Tue, 2 Apr 2024 17:31:56 +0200 Subject: [PATCH 06/11] add forgotten directory --- .github/workflows/check.yaml | 2 +- tests/mocked_plans/conftest.py | 35 +++++ tests/mocked_plans/sample_plans/base.yaml | 155 ++++++++++++++++++++++ tests/mocked_plans/test_base_plan.py | 34 +++++ tests/mocked_plans/utils.py | 77 +++++++++++ 5 files changed, 302 insertions(+), 1 deletion(-) create mode 100644 tests/mocked_plans/conftest.py create mode 100644 tests/mocked_plans/sample_plans/base.yaml create mode 100644 tests/mocked_plans/test_base_plan.py create mode 100644 tests/mocked_plans/utils.py diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index 29058a2c..4058a89f 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -62,7 +62,7 @@ jobs: run: | python -m pip install --upgrade pip python -m pip install tox - - name: Run unit tests + - name: Run mocked-plans tests run: tox -e mocked-plans snap-build: diff --git a/tests/mocked_plans/conftest.py b/tests/mocked_plans/conftest.py new file mode 100644 index 00000000..7fbca1f5 --- /dev/null +++ b/tests/mocked_plans/conftest.py @@ -0,0 +1,35 @@ +# Copyright 2023 Canonical Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from pathlib import Path + +import pytest + +from cou.utils.juju_utils import Model +from tests.mocked_plans.utils import get_sample_plan + + +@pytest.fixture(scope="session") +def sample_plans() -> dict[str, tuple[Model, str]]: + """Fixture that returns all sample plans in a directory. + + This fixture returns a dictionary with filename as key and value as a + cou.utils.juju_utils.Model object whose get_applications function returns the applications + from the file and the expected plan. + """ + directory = Path(__file__).parent / "sample_plans" + + yield { + sample_file.name: get_sample_plan(sample_file) for sample_file in directory.glob("*.yaml") + } diff --git a/tests/mocked_plans/sample_plans/base.yaml b/tests/mocked_plans/sample_plans/base.yaml new file mode 100644 index 00000000..970730eb --- /dev/null +++ b/tests/mocked_plans/sample_plans/base.yaml @@ -0,0 +1,155 @@ +plan: | + Upgrade cloud from 'ussuri' to 'victoria' + Verify that all OpenStack applications are in idle state + Back up MySQL databases + Control Plane principal(s) upgrade plan + Upgrade plan for 'keystone' to 'victoria' + Upgrade software packages of 'keystone' from the current APT repositories + Upgrade software packages on unit 'keystone/0' + Refresh 'keystone' to the latest revision of 'ussuri/stable' + Change charm config of 'keystone' 'action-managed-upgrade' to 'False' + Upgrade 'keystone' to the new channel: 'victoria/stable' + Change charm config of 'keystone' 'openstack-origin' to 'cloud:focal-victoria' + Wait for up to 1800s for model 'base' to reach the idle state + Verify that the workload of 'keystone' has been upgraded on units: keystone/0 + Control Plane subordinate(s) upgrade plan + Upgrade plan for 'keystone-ldap' to 'victoria' + Refresh 'keystone-ldap' to the latest revision of 'ussuri/stable' + Upgrade 'keystone-ldap' to the new channel: 'victoria/stable' + Upgrading all applications deployed on machines with hypervisor. + Upgrade plan for 'az-0' to 'victoria' + Upgrade software packages of 'nova-compute' from the current APT repositories + Upgrade software packages on unit 'nova-compute/0' + Refresh 'nova-compute' to the latest revision of 'ussuri/stable' + Change charm config of 'nova-compute' 'action-managed-upgrade' to 'True' + Upgrade 'nova-compute' to the new channel: 'victoria/stable' + Change charm config of 'nova-compute' 'source' to 'cloud:focal-victoria' + Upgrade plan for units: nova-compute/0 + Upgrade plan for unit 'nova-compute/0' + Disable nova-compute scheduler from unit: 'nova-compute/0' + Verify that unit 'nova-compute/0' has no VMs running + ├── Pause the unit: 'nova-compute/0' + ├── Upgrade the unit: 'nova-compute/0' + ├── Resume the unit: 'nova-compute/0' + Enable nova-compute scheduler from unit: 'nova-compute/0' + Wait for up to 1800s for model 'base' to reach the idle state + Verify that the workload of 'nova-compute' has been upgraded on units: nova-compute/0 + Remaining Data Plane principal(s) upgrade plan + Upgrade plan for 'ceph-osd' to 'victoria' + Verify that all 'nova-compute' units has been upgraded + Upgrade software packages of 'ceph-osd' from the current APT repositories + Upgrade software packages on unit 'ceph-osd/0' + Change charm config of 'ceph-osd' 'source' to 'cloud:focal-victoria' + Wait for up to 300s for app 'ceph-osd' to reach the idle state + Verify that the workload of 'ceph-osd' has been upgraded on units: ceph-osd/0 + Data Plane subordinate(s) upgrade plan + Upgrade plan for 'ovn-chassis' to 'victoria' + Refresh 'ovn-chassis' to the latest revision of '22.03/stable' + +applications: + keystone: + can_upgrade_to: ussuri/stable + charm: keystone + channel: ussuri/stable + config: + openstack-origin: + value: distro + action-managed-upgrade: + value: true + origin: ch + series: focal + subordinate_to: [] + workload_version: 17.0.1 + units: + keystone/0: + name: keystone/0 + machine: '0' + workload_version: 17.0.1 + os_version: ussuri + machines: + '0': + id: '0' + apps: !!python/tuple ['keystone', 'keystone-ldap'] + az: az-0 + + keystone-ldap: + can_upgrade_to: ussuri/stable + charm: keystone-ldap + channel: ussuri/stable + config: {} + origin: ch + series: focal + subordinate_to: + - keystone + workload_version: 17.0.1 + units: {} + machines: + '0': + id: '0' + apps: !!python/tuple ['keystone', 'keystone-ldap'] + az: az-0 + + ceph-osd: + can_upgrade_to: octopus/stable + charm: ceph-osd + channel: octopus/stable + config: + source: + value: distro + origin: ch + series: focal + subordinate_to: [] + workload_version: 17.0.1 + units: + ceph-osd/0: + name: ceph-osd/0 + machine: '2' + workload_version: 17.0.1 + os_version: xena + machines: + '2': + id: '2' + apps: !!python/tuple ['ceph-osd'] + az: az-0 + + nova-compute: + can_upgrade_to: ussuri/stable + charm: nova-compute + channel: ussuri/stable + config: + source: + value: distro + action-managed-upgrade: + value: false + origin: ch + series: focal + subordinate_to: [] + workload_version: 21.0.0 + units: + nova-compute/0: + name: nova-compute/0 + machine: '1' + workload_version: 21.0.0 + os_version: ussuri + machines: + '1': + id: '1' + apps: !!python/tuple ['nova-compute', 'ovn-chassis'] + az: az-0 + + ovn-chassis: + can_upgrade_to: 22.03/stable + charm: ovn-chassis + channel: 22.03/stable + config: {} + origin: ch + series: focal + subordinate_to: + - nova-compute + workload_version: '22.3' + units: {} + machines: + '1': + id: '1' + apps: !!python/tuple ['nova-compute', 'ovn-chassis'] + az: az-0 diff --git a/tests/mocked_plans/test_base_plan.py b/tests/mocked_plans/test_base_plan.py new file mode 100644 index 00000000..b9321d34 --- /dev/null +++ b/tests/mocked_plans/test_base_plan.py @@ -0,0 +1,34 @@ +# Copyright 2023 Canonical Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Test all sample plans.""" +from unittest.mock import patch + +import pytest + +from cou.commands import CLIargs +from cou.steps.analyze import Analysis +from cou.steps.plan import generate_plan + + +@pytest.mark.asyncio +@patch("cou.utils.nova_compute.get_instance_count", return_value=0) +async def test_base_plan(_, sample_plans): + """Testing all sample plans.""" + args = CLIargs("plan", auto_approve=True) + model, exp_plan = sample_plans["base.yaml"] + + analysis_results = await Analysis.create(model) + plan = await generate_plan(analysis_results, args) + + assert str(plan) == exp_plan diff --git a/tests/mocked_plans/utils.py b/tests/mocked_plans/utils.py new file mode 100644 index 00000000..4a2273e3 --- /dev/null +++ b/tests/mocked_plans/utils.py @@ -0,0 +1,77 @@ +# Copyright 2023 Canonical Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Module to provide helper for writing unit tests.""" +from pathlib import Path +from unittest.mock import AsyncMock, PropertyMock + +import yaml + +from cou.utils.juju_utils import Application, Machine, Model, Unit +from tests.unit.utils import dedent_plan + + +def get_sample_plan(source: Path) -> tuple[Model, str]: + """Help function to get dict of Applications and expected upgrade plan from file. + + This function can load applications from yaml format, where each app is string representation + of OpenStackApplication (str(OpenStackApplication), see OpenStackApplication.__str__). + + applications: + : + model_name: ... + can_upgrade_to: ... + ... + : + model_name: ... + can_upgrade_to: ... + ... + plan: | + ... + """ + with open(source, "r") as file: + data = yaml.load(file, Loader=yaml.Loader) + + model = AsyncMock(spec_set=Model) + + # Note(rgildein): We need to get machines first, since they are used in Unit object. + machines = { + machine_id: Machine(machine["id"], machine["apps"], machine["az"]) + for app_data in data["applications"].values() + for machine_id, machine in app_data["machines"].items() + } + applications = { + name: Application( + name=name, + can_upgrade_to=app_data["can_upgrade_to"], + charm=app_data["charm"], + channel=app_data["channel"], + config=app_data["config"], + machines={machine_id: machines[machine_id] for machine_id in app_data["machines"]}, + model=model, + origin=app_data["origin"], + series=app_data["series"], + subordinate_to=app_data["subordinate_to"], + units={ + name: Unit(name, machines[unit["machine"]], unit["workload_version"]) + for name, unit in app_data["units"].items() + }, + workload_version=app_data["workload_version"], + ) + for name, app_data in data["applications"].items() + } + + type(model).name = PropertyMock(return_value=source.stem) + model.get_applications = AsyncMock(return_value=applications) + + return model, dedent_plan(data["plan"]) From bb67c9692400221562439f922c385967dac516a6 Mon Sep 17 00:00:00 2001 From: Robert Gildein Date: Tue, 2 Apr 2024 19:35:46 +0200 Subject: [PATCH 07/11] fix ovn-chassis after merging main --- tests/mocked_plans/sample_plans/base.yaml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/mocked_plans/sample_plans/base.yaml b/tests/mocked_plans/sample_plans/base.yaml index 970730eb..9ed3d199 100644 --- a/tests/mocked_plans/sample_plans/base.yaml +++ b/tests/mocked_plans/sample_plans/base.yaml @@ -18,6 +18,7 @@ plan: | Upgrade 'keystone-ldap' to the new channel: 'victoria/stable' Upgrading all applications deployed on machines with hypervisor. Upgrade plan for 'az-0' to 'victoria' + Disable nova-compute scheduler from unit: 'nova-compute/0' Upgrade software packages of 'nova-compute' from the current APT repositories Upgrade software packages on unit 'nova-compute/0' Refresh 'nova-compute' to the latest revision of 'ussuri/stable' @@ -26,12 +27,11 @@ plan: | Change charm config of 'nova-compute' 'source' to 'cloud:focal-victoria' Upgrade plan for units: nova-compute/0 Upgrade plan for unit 'nova-compute/0' - Disable nova-compute scheduler from unit: 'nova-compute/0' Verify that unit 'nova-compute/0' has no VMs running ├── Pause the unit: 'nova-compute/0' ├── Upgrade the unit: 'nova-compute/0' ├── Resume the unit: 'nova-compute/0' - Enable nova-compute scheduler from unit: 'nova-compute/0' + Enable nova-compute scheduler from unit: 'nova-compute/0' Wait for up to 1800s for model 'base' to reach the idle state Verify that the workload of 'nova-compute' has been upgraded on units: nova-compute/0 Remaining Data Plane principal(s) upgrade plan @@ -141,7 +141,9 @@ applications: can_upgrade_to: 22.03/stable charm: ovn-chassis channel: 22.03/stable - config: {} + config: + enable-version-pinning: + value: false origin: ch series: focal subordinate_to: From 0d83ca33771d68e2a0a4da7f046a34e30abb33fc Mon Sep 17 00:00:00 2001 From: Robert Gildein Date: Wed, 3 Apr 2024 23:33:06 +0200 Subject: [PATCH 08/11] Update tests/mocked_plans/utils.py Co-authored-by: TQ X --- tests/mocked_plans/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/mocked_plans/utils.py b/tests/mocked_plans/utils.py index 4a2273e3..d0516bc5 100644 --- a/tests/mocked_plans/utils.py +++ b/tests/mocked_plans/utils.py @@ -11,7 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -"""Module to provide helper for writing unit tests.""" +"""Module to provide helper functions for writing mock upgrade tests.""" from pathlib import Path from unittest.mock import AsyncMock, PropertyMock From 77d33ddf14fa4d919bba6ba66835ed7594e021d0 Mon Sep 17 00:00:00 2001 From: Robert Gildein Date: Wed, 3 Apr 2024 23:33:15 +0200 Subject: [PATCH 09/11] Update tests/mocked_plans/test_base_plan.py Co-authored-by: TQ X --- tests/mocked_plans/test_base_plan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/mocked_plans/test_base_plan.py b/tests/mocked_plans/test_base_plan.py index b9321d34..3614665b 100644 --- a/tests/mocked_plans/test_base_plan.py +++ b/tests/mocked_plans/test_base_plan.py @@ -24,7 +24,7 @@ @pytest.mark.asyncio @patch("cou.utils.nova_compute.get_instance_count", return_value=0) async def test_base_plan(_, sample_plans): - """Testing all sample plans.""" + """Testing the base plans.""" args = CLIargs("plan", auto_approve=True) model, exp_plan = sample_plans["base.yaml"] From d55cb85a55587634cedb9edc8c2c4faeb0412d3a Mon Sep 17 00:00:00 2001 From: Robert Gildein Date: Wed, 3 Apr 2024 23:33:30 +0200 Subject: [PATCH 10/11] Update tests/mocked_plans/conftest.py Co-authored-by: TQ X --- tests/mocked_plans/conftest.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/mocked_plans/conftest.py b/tests/mocked_plans/conftest.py index 7fbca1f5..938e692e 100644 --- a/tests/mocked_plans/conftest.py +++ b/tests/mocked_plans/conftest.py @@ -24,9 +24,11 @@ def sample_plans() -> dict[str, tuple[Model, str]]: """Fixture that returns all sample plans in a directory. - This fixture returns a dictionary with filename as key and value as a - cou.utils.juju_utils.Model object whose get_applications function returns the applications - from the file and the expected plan. + This fixture returns a dictionary with the filename as the key and + a tuple consisting of a cou.utils.juju_utils.Model object and the + expected plan in string format as the value. The get_applications + function of this Model object returns the applications read from a + YAML file, from which the expected plan is also parsed. """ directory = Path(__file__).parent / "sample_plans" From e5f2e1ef6f0d26d828cef9bd4bebaa9e8790049a Mon Sep 17 00:00:00 2001 From: Robert Gildein Date: Wed, 3 Apr 2024 23:37:41 +0200 Subject: [PATCH 11/11] fix suggested changes --- tests/mocked_plans/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/mocked_plans/conftest.py b/tests/mocked_plans/conftest.py index 938e692e..b8708d40 100644 --- a/tests/mocked_plans/conftest.py +++ b/tests/mocked_plans/conftest.py @@ -27,7 +27,7 @@ def sample_plans() -> dict[str, tuple[Model, str]]: This fixture returns a dictionary with the filename as the key and a tuple consisting of a cou.utils.juju_utils.Model object and the expected plan in string format as the value. The get_applications - function of this Model object returns the applications read from a + function of this Model object returns the applications read from a YAML file, from which the expected plan is also parsed. """ directory = Path(__file__).parent / "sample_plans"