From 8add88fd856998e528e5747837379d1656d219cb Mon Sep 17 00:00:00 2001 From: Gabriel Cocenza Date: Thu, 18 Jan 2024 13:50:33 -0300 Subject: [PATCH] Custom COU Namespace (#221) - create a new dataclass to be the COU Namespace. With that, type is still checked and few arguments are necessary to pass to the functions that use the arguments passed by the user. Moreover, docstrings are shorter and easier to maintain and frozen don't let accidentally change the object. - Global variables for "data-plane" and "control-plane". --- cou/cli.py | 62 +++++++++----------------- cou/commands.py | 47 +++++++++++++++++--- cou/steps/plan.py | 14 +++--- tests/unit/conftest.py | 12 +++++ tests/unit/steps/test_steps_plan.py | 4 +- tests/unit/test_cli.py | 69 ++++++++++++++++++----------- tests/unit/test_commands.py | 57 ++++++++++++------------ 7 files changed, 153 insertions(+), 112 deletions(-) diff --git a/cou/cli.py b/cou/cli.py index ff681f3b..25dbb48d 100644 --- a/cou/cli.py +++ b/cou/cli.py @@ -13,18 +13,16 @@ # limitations under the License. """Entrypoint for 'charmed-openstack-upgrader'.""" -import argparse import asyncio import logging import logging.handlers import sys from enum import Enum from signal import SIGINT, SIGTERM -from typing import Optional from juju.errors import JujuError -from cou.commands import parse_args +from cou.commands import CLIargs, parse_args from cou.exceptions import COUException, HighestReleaseAchieved, TimeoutException from cou.logging import setup_logging from cou.steps import UpgradePlan @@ -107,19 +105,15 @@ async def continue_upgrade() -> bool: return False -async def analyze_and_plan( - model_name: Optional[str], backup_database: bool -) -> tuple[Analysis, UpgradePlan]: +async def analyze_and_plan(args: CLIargs) -> tuple[Analysis, UpgradePlan]: """Analyze cloud and generate the upgrade plan with steps. - :param model_name: Model name inputted by user. - :type model_name: Optional[str] - :param backup_database: Whether to create database backup before upgrade. - :type backup_database: bool + :param args: CLI arguments + :type args: CLIargs :return: Generated analysis and upgrade plan. :rtype: tuple[Analysis, UpgradePlan] """ - model = COUModel(model_name) + model = COUModel(args.model_name) progress_indicator.start(f"Connecting to '{model.name}' model...") await model.connect() logger.info("Using model: %s", model.name) @@ -131,21 +125,19 @@ async def analyze_and_plan( progress_indicator.succeed() progress_indicator.start("Generating upgrade plan...") - upgrade_plan = await generate_plan(analysis_result, backup_database) + upgrade_plan = await generate_plan(analysis_result, args) progress_indicator.succeed() return analysis_result, upgrade_plan -async def get_upgrade_plan(model_name: Optional[str], backup_database: bool) -> None: +async def get_upgrade_plan(args: CLIargs) -> None: """Get upgrade plan and print to console. - :param model_name: Model name inputted by user. - :type model_name: Optional[str] - :param backup_database: Whether to create database backup before upgrade. - :type backup_database: bool + :param args: CLI arguments + :type args: CLIargs """ - analysis_result, upgrade_plan = await analyze_and_plan(model_name, backup_database) + analysis_result, upgrade_plan = await analyze_and_plan(args) print_and_debug(upgrade_plan) manually_upgrade_data_plane(analysis_result) print( @@ -154,27 +146,16 @@ async def get_upgrade_plan(model_name: Optional[str], backup_database: bool) -> ) -async def run_upgrade( - model_name: Optional[str], - backup_database: bool, - prompt: bool, - quiet: bool, -) -> None: +async def run_upgrade(args: CLIargs) -> None: """Run cloud upgrade. - :param model_name: Model name inputted by user. - :type model_name: Optional[str] - :param backup_database: Whether to create database backup before upgrade. - :type backup_database: bool - :param prompt: Whether to prompt to run upgrade interactively. - :type prompt: bool - :param quiet: Whether to run upgrade in quiet mode. - :type quiet: bool + :param args: CLI arguments + :type args: CLIargs """ - analysis_result, upgrade_plan = await analyze_and_plan(model_name, backup_database) + analysis_result, upgrade_plan = await analyze_and_plan(args) print_and_debug(upgrade_plan) - if prompt and not await continue_upgrade(): + if args.prompt and not await continue_upgrade(): return # NOTE(rgildein): add handling upgrade plan canceling for SIGINT (ctrl+c) and SIGTERM @@ -183,26 +164,25 @@ async def run_upgrade( loop.add_signal_handler(SIGTERM, interrupt_handler, upgrade_plan, loop, 143) # don't print plan if in quiet mode - if not quiet: + if not args.quiet: print("Running cloud upgrade...") - await apply_step(upgrade_plan, prompt) + await apply_step(upgrade_plan, args.prompt) manually_upgrade_data_plane(analysis_result) print("Upgrade completed.") -async def _run_command(args: argparse.Namespace) -> None: +async def _run_command(args: CLIargs) -> None: """Run 'charmed-openstack-upgrade' command. :param args: CLI arguments - :type args: argparse.Namespace + :type args: CLIargs """ match args.command: case "plan": - await get_upgrade_plan(args.model_name, args.backup) + await get_upgrade_plan(args) case "upgrade": - prompt = not args.auto_approve - await run_upgrade(args.model_name, args.backup, prompt, args.quiet) + await run_upgrade(args) def entrypoint() -> None: diff --git a/cou/commands.py b/cou/commands.py index 1d36de2e..c115cbda 100644 --- a/cou/commands.py +++ b/cou/commands.py @@ -14,10 +14,14 @@ """Command line arguments parsing for 'charmed-openstack-upgrader'.""" import argparse +from dataclasses import dataclass from typing import Any, Iterable, Optional import pkg_resources +CONTROL_PLANE = "control-plane" +DATA_PLANE = "data-plane" + class CapitalizeHelpFormatter(argparse.RawTextHelpFormatter): """Capitalize message prefix.""" @@ -194,7 +198,7 @@ def create_plan_subparser( # help="For more information about a upgrade group, run 'cou plan ' -h.", ) plan_subparser.add_parser( - "control-plane", + CONTROL_PLANE, description="Show the steps for upgrading the control-plane components.", help="Show the steps for upgrading the control-plane components.", usage="cou plan control-plane [options]", @@ -202,7 +206,7 @@ def create_plan_subparser( formatter_class=CapitalizeHelpFormatter, ) plan_subparser.add_parser( - "data-plane", + DATA_PLANE, description="Show the steps for upgrading the data-plane components.\nThis is possible " "only if control-plane has been fully upgraded,\notherwise an error will be thrown.", help="Show the steps for upgrading the data-plane components.\nThis is possible " @@ -316,13 +320,44 @@ def create_subparsers(parser: argparse.ArgumentParser) -> argparse._SubParsersAc return subparsers -def parse_args(args: Any) -> argparse.Namespace: # pylint: disable=inconsistent-return-statements +@dataclass(frozen=True) +class CLIargs: + """Wrap CLI arguments instead of using argparse.Namespace. + + Keep in sync with the argument parser defined in parse_args and check types. + """ + + # pylint: disable=too-many-instance-attributes + + command: str + verbosity: int = 0 + backup: bool = True + quiet: bool = False + auto_approve: bool = False + model_name: Optional[str] = None + upgrade_group: Optional[str] = None + subcommand: Optional[str] = None # for help option + machines: Optional[list[str]] = None + hostnames: Optional[list[str]] = None + availability_zones: Optional[list[str]] = None + + @property + def prompt(self) -> bool: + """Whether if COU should prompt to the user. + + :return: Prompt if auto_approve is True, otherwise don't prompt. + :rtype: bool + """ + return not self.auto_approve + + +def parse_args(args: Any) -> CLIargs: # pylint: disable=inconsistent-return-statements """Parse cli arguments. :param args: Arguments parser. :type args: Any - :return: argparse.Namespace - :rtype: argparse.Namespace + :return: CLIargs custom object. + :rtype: CLIargs :raises argparse.ArgumentError: Unexpected arguments input. """ # Configure top level argparser and its options @@ -354,7 +389,7 @@ def parse_args(args: Any) -> argparse.Namespace: # pylint: disable=inconsistent parser.exit() try: - parsed_args = parser.parse_args(args) + parsed_args = CLIargs(**vars(parser.parse_args(args))) # print help messages for an available sub-command if parsed_args.command == "help": diff --git a/cou/steps/plan.py b/cou/steps/plan.py index e54ce68d..4a3e8030 100644 --- a/cou/steps/plan.py +++ b/cou/steps/plan.py @@ -37,6 +37,7 @@ OpenStackSubordinateApplication, SubordinateBaseClass, ) +from cou.commands import CLIargs from cou.exceptions import ( HaltUpgradePlanGeneration, HighestReleaseAchieved, @@ -52,18 +53,13 @@ logger = logging.getLogger(__name__) -async def generate_plan(analysis_result: Analysis, backup_database: bool) -> UpgradePlan: +async def generate_plan(analysis_result: Analysis, args: CLIargs) -> UpgradePlan: """Generate plan for upgrade. :param analysis_result: Analysis result. :type analysis_result: Analysis - :param backup_database: Whether to create database backup before upgrade. - :type backup_database: bool - :raises NoTargetError: When cannot find target to upgrade. - :raises HighestReleaseAchieved: When the highest possible OpenStack release is - already achieved. - :raises OutOfSupportRange: When the OpenStack release or Ubuntu series is out of the current - supporting range. + :param args: CLI arguments + :type args: CLIargs :return: Plan with all upgrade steps necessary based on the Analysis. :rtype: UpgradePlan """ @@ -89,7 +85,7 @@ async def generate_plan(analysis_result: Analysis, backup_database: bool) -> Upg ), ) ) - if backup_database: + if args.backup: plan.add_step( PreUpgradeStep( description="Backup mysql databases", diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index cf9b71dc..d6883940 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -25,6 +25,7 @@ from cou.apps.base import ApplicationUnit, OpenStackApplication from cou.apps.core import Keystone from cou.apps.subordinate import OpenStackSubordinateApplication +from cou.commands import CLIargs from cou.utils.openstack import OpenStackRelease @@ -575,3 +576,14 @@ def cou_data(tmp_path_factory): cou_test = tmp_path_factory.mktemp("cou_test") with patch("cou.utils.COU_DATA", cou_test): yield + + +@pytest.fixture +def cli_args() -> MagicMock: + """Magic Mock of the COU CLIargs. + + :return: MagicMock of the COU CLIargs got from the cli. + :rtype: MagicMock + """ + # spec_set needs an instantiated class to be strict with the fields. + return MagicMock(spec_set=CLIargs(command="plan"))() diff --git a/tests/unit/steps/test_steps_plan.py b/tests/unit/steps/test_steps_plan.py index fdfc6d89..360dc73a 100644 --- a/tests/unit/steps/test_steps_plan.py +++ b/tests/unit/steps/test_steps_plan.py @@ -131,7 +131,7 @@ def generate_expected_upgrade_plan_subordinate(app, target, model): @pytest.mark.asyncio -async def test_generate_plan(apps, model): +async def test_generate_plan(apps, model, cli_args): target = OpenStackRelease("victoria") app_keystone = apps["keystone_ussuri"] app_cinder = apps["cinder_ussuri"] @@ -142,7 +142,7 @@ async def test_generate_plan(apps, model): apps_data_plane=[], ) - upgrade_plan = await generate_plan(analysis_result, backup_database=True) + upgrade_plan = await generate_plan(analysis_result, cli_args) expected_plan = UpgradePlan("Upgrade cloud from 'ussuri' to 'victoria'") expected_plan.add_step( diff --git a/tests/unit/test_cli.py b/tests/unit/test_cli.py index e79b2875..d161be9d 100644 --- a/tests/unit/test_cli.py +++ b/tests/unit/test_cli.py @@ -63,17 +63,20 @@ def test_get_log_level(quiet, verbosity, level): @patch("cou.cli.COUModel") @patch("cou.cli.generate_plan", new_callable=AsyncMock) @patch("cou.cli.Analysis.create", new_callable=AsyncMock) -async def test_analyze_and_plan(mock_analyze, mock_generate_plan, cou_model): +async def test_analyze_and_plan(mock_analyze, mock_generate_plan, cou_model, cli_args): """Test analyze_and_plan function with different model_name arguments.""" + cli_args.model_name = None + cli_args.backup = False + cou_model.return_value.connect.side_effect = AsyncMock() analysis_result = Analysis(model=cou_model, apps_control_plane=[], apps_data_plane=[]) mock_analyze.return_value = analysis_result - await cli.analyze_and_plan(None, False) + await cli.analyze_and_plan(cli_args) cou_model.assert_called_once_with(None) mock_analyze.assert_awaited_once_with(cou_model.return_value) - mock_generate_plan.assert_awaited_once_with(analysis_result, False) + mock_generate_plan.assert_awaited_once_with(analysis_result, cli_args) @pytest.mark.asyncio @@ -81,7 +84,7 @@ async def test_analyze_and_plan(mock_analyze, mock_generate_plan, cou_model): @patch("cou.cli.analyze_and_plan", new_callable=AsyncMock) @patch("cou.cli.print_and_debug") async def test_get_upgrade_plan( - mock_print_and_debug, mock_analyze_and_plan, mock_manually_upgrade + mock_print_and_debug, mock_analyze_and_plan, mock_manually_upgrade, cli_args ): """Test get_upgrade_plan function.""" plan = UpgradePlan(description="Upgrade cloud from 'ussuri' to 'victoria'") @@ -89,9 +92,9 @@ async def test_get_upgrade_plan( mock_analysis_result = MagicMock() mock_analyze_and_plan.return_value = (mock_analysis_result, plan) - await cli.get_upgrade_plan(None, True) + await cli.get_upgrade_plan(cli_args) - mock_analyze_and_plan.assert_awaited_once_with(None, True) + mock_analyze_and_plan.assert_awaited_once_with(cli_args) mock_print_and_debug.assert_called_once_with(plan) mock_manually_upgrade.assert_called_once() @@ -104,29 +107,36 @@ async def test_get_upgrade_plan( (False, 0), ], ) +@patch("cou.cli.continue_upgrade", new_callable=AsyncMock) @patch("cou.cli.manually_upgrade_data_plane") @patch("cou.cli.analyze_and_plan", new_callable=AsyncMock) @patch("cou.cli.apply_step") @patch("builtins.print") @patch("cou.cli.print_and_debug") -async def test_run_upgrade_quiet( +async def test_run_upgrade_quiet_no_prompt( mock_print_and_debug, mock_print, mock_apply_step, mock_analyze_and_plan, mock_manually_upgrade, + mock_continue_upgrade, quiet, expected_print_count, + cli_args, ): - """Test get_upgrade_plan function in either quiet or non-quiet mode.""" + """Test get_upgrade_plan function in either quiet or non-quiet mode without prompt.""" + mock_continue_upgrade.return_value = True + cli_args.quiet = quiet + cli_args.prompt = False + plan = UpgradePlan(description="Upgrade cloud from 'ussuri' to 'victoria'") plan.add_step(PreUpgradeStep(description="backup mysql databases", parallel=False)) mock_analysis_result = MagicMock() mock_analyze_and_plan.return_value = (mock_analysis_result, plan) - await cli.run_upgrade(model_name=None, backup_database=True, prompt=False, quiet=quiet) + await cli.run_upgrade(cli_args) - mock_analyze_and_plan.assert_awaited_once_with(None, True) + mock_analyze_and_plan.assert_awaited_once_with(cli_args) mock_print_and_debug.assert_called_once_with(plan) mock_apply_step.assert_called_once_with(plan, False) mock_print.call_count == expected_print_count @@ -143,16 +153,20 @@ async def test_run_upgrade_with_prompt_continue( mock_apply_step, mock_analyze_and_plan, mock_manually_upgrade, + cli_args, ): + cli_args.prompt = True + cli_args.quiet = True + plan = UpgradePlan(description="Upgrade cloud from 'ussuri' to 'victoria'") plan.add_step(PreUpgradeStep(description="backup mysql databases", parallel=False)) mock_analysis_result = MagicMock() mock_analyze_and_plan.return_value = (mock_analysis_result, plan) mock_continue_upgrade.return_value = True - await cli.run_upgrade(model_name=None, backup_database=True, prompt=True, quiet=False) + await cli.run_upgrade(cli_args) - mock_analyze_and_plan.assert_awaited_once_with(None, True) + mock_analyze_and_plan.assert_awaited_once_with(cli_args) mock_continue_upgrade.assert_awaited_once_with() mock_apply_step.assert_called_once_with(plan, True) mock_manually_upgrade.assert_called_once() @@ -168,16 +182,20 @@ async def test_run_upgrade_with_prompt_abort( mock_apply_step, mock_analyze_and_plan, mock_manually_upgrade, + cli_args, ): + cli_args.auto_approve = False + cli_args.quiet = True + plan = UpgradePlan(description="Upgrade cloud from 'ussuri' to 'victoria'") plan.add_step(PreUpgradeStep(description="backup mysql databases", parallel=False)) mock_analysis_result = MagicMock() mock_analyze_and_plan.return_value = (mock_analysis_result, plan) mock_continue_upgrade.return_value = False - await cli.run_upgrade(model_name=None, backup_database=True, prompt=True, quiet=False) + await cli.run_upgrade(cli_args) - mock_analyze_and_plan.assert_awaited_once_with(None, True) + mock_analyze_and_plan.assert_awaited_once_with(cli_args) mock_continue_upgrade.assert_awaited_once_with() mock_apply_step.assert_not_awaited() mock_manually_upgrade.assert_not_called() @@ -193,15 +211,19 @@ async def test_run_upgrade_with_no_prompt( mock_apply_step, mock_analyze_and_plan, mock_manually_upgrade, + cli_args, ): + cli_args.prompt = False + cli_args.quiet = True + plan = UpgradePlan(description="Upgrade cloud from 'ussuri' to 'victoria'") plan.add_step(PreUpgradeStep(description="backup mysql databases", parallel=False)) mock_analysis_result = MagicMock() mock_analyze_and_plan.return_value = (mock_analysis_result, plan) - await cli.run_upgrade(model_name=None, backup_database=True, prompt=False, quiet=False) + await cli.run_upgrade(cli_args) - mock_analyze_and_plan.assert_awaited_once_with(None, True) + mock_analyze_and_plan.assert_awaited_once_with(cli_args) mock_continue_upgrade.assert_not_awaited() mock_apply_step.assert_called_once_with(plan, False) mock_manually_upgrade.assert_called_once() @@ -234,22 +256,17 @@ async def test_continue_upgrade( @pytest.mark.parametrize("command", ["plan", "upgrade", "other1", "other2"]) @patch("cou.cli.get_upgrade_plan") @patch("cou.cli.run_upgrade") -async def test_run_command(mock_run_upgrade, mock_get_upgrade_plan, command): +async def test_run_command(mock_run_upgrade, mock_get_upgrade_plan, command, cli_args): """Test run command function.""" - args = MagicMock(spec="argparse.Namespace")() - args.command = command - prompt = not args.auto_approve + cli_args.command = command - await cli._run_command(args) + await cli._run_command(cli_args) if command == "plan": - mock_get_upgrade_plan.assert_awaited_once_with( - args.model_name, - args.backup, - ) + mock_get_upgrade_plan.assert_awaited_once_with(cli_args) mock_run_upgrade.assert_not_called() elif command == "upgrade": - mock_run_upgrade.assert_awaited_once_with(args.model_name, args.backup, prompt, args.quiet) + mock_run_upgrade.assert_awaited_once_with(cli_args) mock_get_upgrade_plan.assert_not_called() else: mock_run_upgrade.assert_not_called() diff --git a/tests/unit/test_commands.py b/tests/unit/test_commands.py index 5f4d7287..0e25bb50 100644 --- a/tests/unit/test_commands.py +++ b/tests/unit/test_commands.py @@ -12,12 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -from argparse import ArgumentParser, Namespace +from argparse import ArgumentParser from unittest.mock import patch import pytest from cou import commands +from cou.commands import CLIargs @pytest.mark.parametrize( @@ -61,11 +62,11 @@ def test_parse_args_quiet_verbose_exclusive(args): @pytest.mark.parametrize( - "args, expected_namespace", + "args, expected_CLIargs", [ ( ["plan"], - Namespace( + CLIargs( command="plan", model_name=None, verbosity=0, @@ -76,7 +77,7 @@ def test_parse_args_quiet_verbose_exclusive(args): ), ( ["plan", "--no-backup"], - Namespace( + CLIargs( command="plan", model_name=None, verbosity=0, @@ -87,7 +88,7 @@ def test_parse_args_quiet_verbose_exclusive(args): ), ( ["plan", "--no-backup", "--quiet"], - Namespace( + CLIargs( command="plan", model_name=None, verbosity=0, @@ -98,7 +99,7 @@ def test_parse_args_quiet_verbose_exclusive(args): ), ( ["plan", "--model=model_name"], - Namespace( + CLIargs( command="plan", model_name="model_name", verbosity=0, @@ -109,7 +110,7 @@ def test_parse_args_quiet_verbose_exclusive(args): ), ( ["plan", "control-plane"], - Namespace( + CLIargs( command="plan", model_name=None, verbosity=0, @@ -120,7 +121,7 @@ def test_parse_args_quiet_verbose_exclusive(args): ), ( ["plan", "data-plane"], - Namespace( + CLIargs( command="plan", model_name=None, verbosity=0, @@ -134,7 +135,7 @@ def test_parse_args_quiet_verbose_exclusive(args): ), ( ["plan", "control-plane", "--verbose"], - Namespace( + CLIargs( command="plan", model_name=None, verbosity=1, @@ -145,7 +146,7 @@ def test_parse_args_quiet_verbose_exclusive(args): ), ( ["plan", "data-plane", "--machine=1", "-m=2,3"], - Namespace( + CLIargs( command="plan", model_name=None, verbosity=0, @@ -159,7 +160,7 @@ def test_parse_args_quiet_verbose_exclusive(args): ), ( ["plan", "data-plane", "--quiet", "--availability-zone=1", "--az=2,3"], - Namespace( + CLIargs( command="plan", model_name=None, verbosity=0, @@ -173,7 +174,7 @@ def test_parse_args_quiet_verbose_exclusive(args): ), ( ["plan", "data-plane", "--hostname=1", "-n=2,3"], - Namespace( + CLIargs( command="plan", model_name=None, verbosity=0, @@ -187,19 +188,19 @@ def test_parse_args_quiet_verbose_exclusive(args): ), ], ) -def test_parse_args_plan(args, expected_namespace): +def test_parse_args_plan(args, expected_CLIargs): """Test parsing 'plan' subcommand and its arguments/options.""" parsed_args = commands.parse_args(args) - assert parsed_args == expected_namespace + assert parsed_args == expected_CLIargs @pytest.mark.parametrize( - "args, expected_namespace", + "args, expected_CLIargs", [ ( ["upgrade"], - Namespace( + CLIargs( command="upgrade", model_name=None, verbosity=0, @@ -211,7 +212,7 @@ def test_parse_args_plan(args, expected_namespace): ), ( ["upgrade", "--no-backup"], - Namespace( + CLIargs( command="upgrade", model_name=None, verbosity=0, @@ -223,7 +224,7 @@ def test_parse_args_plan(args, expected_namespace): ), ( ["upgrade", "--no-backup", "--quiet"], - Namespace( + CLIargs( command="upgrade", model_name=None, verbosity=0, @@ -235,7 +236,7 @@ def test_parse_args_plan(args, expected_namespace): ), ( ["upgrade", "--model=model_name"], - Namespace( + CLIargs( command="upgrade", model_name="model_name", verbosity=0, @@ -247,7 +248,7 @@ def test_parse_args_plan(args, expected_namespace): ), ( ["upgrade", "control-plane"], - Namespace( + CLIargs( command="upgrade", model_name=None, verbosity=0, @@ -259,7 +260,7 @@ def test_parse_args_plan(args, expected_namespace): ), ( ["upgrade", "data-plane"], - Namespace( + CLIargs( command="upgrade", model_name=None, verbosity=0, @@ -274,7 +275,7 @@ def test_parse_args_plan(args, expected_namespace): ), ( ["upgrade", "control-plane", "--verbose"], - Namespace( + CLIargs( command="upgrade", model_name=None, verbosity=1, @@ -286,7 +287,7 @@ def test_parse_args_plan(args, expected_namespace): ), ( ["upgrade", "data-plane", "--machine=1", "-m=2,3"], - Namespace( + CLIargs( command="upgrade", model_name=None, verbosity=0, @@ -301,7 +302,7 @@ def test_parse_args_plan(args, expected_namespace): ), ( ["upgrade", "data-plane", "--quiet", "--availability-zone=1", "--az=2,3"], - Namespace( + CLIargs( command="upgrade", model_name=None, verbosity=0, @@ -316,7 +317,7 @@ def test_parse_args_plan(args, expected_namespace): ), ( ["upgrade", "data-plane", "--hostname=1", "-n=2,3"], - Namespace( + CLIargs( command="upgrade", model_name=None, verbosity=0, @@ -331,7 +332,7 @@ def test_parse_args_plan(args, expected_namespace): ), ( ["upgrade", "data-plane", "--auto-approve", "--hostname=1", "-n=2,3"], - Namespace( + CLIargs( command="upgrade", model_name=None, verbosity=0, @@ -346,11 +347,11 @@ def test_parse_args_plan(args, expected_namespace): ), ], ) -def test_parse_args_upgrade(args, expected_namespace): +def test_parse_args_upgrade(args, expected_CLIargs): """Test parsing 'run' subcommand and its arguments/options.""" parsed_args = commands.parse_args(args) - assert parsed_args == expected_namespace + assert parsed_args == expected_CLIargs @pytest.mark.parametrize(