diff --git a/conftest.py b/conftest.py index 2387199374..ee32829133 100644 --- a/conftest.py +++ b/conftest.py @@ -33,6 +33,7 @@ from utilities.bitwarden import get_cnv_tests_secret_by_name from utilities.constants import ( AMD_64, + MULTIARCH, QUARANTINED, SETUP_ERROR, TIMEOUT_5MIN, @@ -558,6 +559,15 @@ def filter_sno_only_tests(items: list[Item], config: Config) -> list[Item]: return items +def filter_multiarch_tests(items: list[Item], config: Config) -> list[Item]: + if py_config.get("cluster_type") == MULTIARCH: + return items + discard_tests, items_to_return = remove_tests_from_list(items=items, filter_str="multiarch") + if discard_tests: + config.hook.pytest_deselected(items=discard_tests) + return items_to_return + + def remove_tests_from_list(items: list[Item], filter_str: str) -> tuple[list[Item], list[Item]]: discard_tests: list[Item] = [] items_to_return: list[Item] = [] @@ -635,6 +645,7 @@ def pytest_collection_modifyitems(session, config, items): config.hook.pytest_deselected(items=discard) items[:] = filter_deprecated_api_tests(items=items, config=config) items[:] = filter_sno_only_tests(items=items, config=config) + items[:] = filter_multiarch_tests(items=items, config=config) items[:] = mark_nmstate_dependent_tests(items=items) diff --git a/tests/conftest.py b/tests/conftest.py index 284eb82dad..72bde0f1fa 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -73,6 +73,7 @@ from libs.net.ip import filter_link_local_addresses, random_ipv4_address, random_ipv6_address from libs.net.vmspec import lookup_iface_status from tests.utils import download_and_extract_tar +from utilities.architecture import get_cluster_architecture from utilities.artifactory import get_artifactory_header, get_http_image_url, get_test_artifact_server_url from utilities.bitwarden import get_cnv_tests_secret_by_name from utilities.cluster import cache_admin_client, get_oc_whoami_username @@ -446,6 +447,7 @@ def schedulable_nodes(nodes): """ schedulable_label = "kubevirt.io/schedulable" cpu_arch = py_config.get("cpu_arch") + cpu_archs = [cpu_arch] if isinstance(cpu_arch, str) else cpu_arch schedulable = [ node for node in nodes @@ -454,7 +456,7 @@ def schedulable_nodes(nodes): and not node.instance.spec.unschedulable and not kubernetes_taint_exists(node) and node.kubelet_ready - and (not cpu_arch or node.labels.get(KUBERNETES_ARCH_LABEL) == cpu_arch) + and (not cpu_archs or node.labels.get(KUBERNETES_ARCH_LABEL) in cpu_archs) ] LOGGER.info(f"Schedulable nodes: {[node.name for node in schedulable]}, node architecture: {cpu_arch or 'all'}") @@ -1026,7 +1028,7 @@ def mac_pool(admin_client, hco_namespace): @pytest.fixture(scope="session") def nodes_cpu_architecture(): - return py_config["cpu_arch"] + return py_config.get("cpu_arch") @pytest.fixture(scope="session") @@ -1478,7 +1480,7 @@ def cluster_info( f"\tOCS version: {ocs_current_version}\n" f"\tCNI type: {get_cluster_cni_type(admin_client=admin_client)}\n" f"\tWorkers type: {workers_type}\n" - f"\tCluster CPU Architecture: {nodes_cpu_architecture}\n" + f"\tCluster CPU Architecture: {nodes_cpu_architecture or ', '.join(sorted(get_cluster_architecture()))}\n" f"\tIPv4 cluster: {ipv4_supported_cluster()}\n" f"\tIPv6 cluster: {ipv6_supported_cluster()}\n" f"\tVirtctl version: \n\t{virtctl_client_version}\n\t{virtctl_server_version}\n" diff --git a/tests/install_upgrade_operators/constants.py b/tests/install_upgrade_operators/constants.py index 38478d2ddc..fee4faf96e 100644 --- a/tests/install_upgrade_operators/constants.py +++ b/tests/install_upgrade_operators/constants.py @@ -1,3 +1,8 @@ +from ocp_resources.cdi import CDI +from ocp_resources.kubevirt import KubeVirt +from ocp_resources.network_addons_config import NetworkAddonsConfig +from ocp_resources.ssp import SSP + SECTION_TITLE = "section_title" FILE_SUFFIX = "file_suffix" HCO_CR_CERT_CONFIG_CA_KEY = "ca" @@ -58,6 +63,7 @@ "containerPathVolumes": FG_DISABLED, } CUSTOM_DATASOURCE_NAME = "custom-datasource" +MANAGED_CRS_LIST = [KubeVirt, CDI, NetworkAddonsConfig, SSP] WORKLOAD_UPDATE_STRATEGY_KEY_NAME = "workloadUpdateStrategy" KUBEMACPOOL_SERVICE = "kubemacpool-service" diff --git a/tests/install_upgrade_operators/crypto_policy/constants.py b/tests/install_upgrade_operators/crypto_policy/constants.py index d5f98b6467..09ee41cce0 100644 --- a/tests/install_upgrade_operators/crypto_policy/constants.py +++ b/tests/install_upgrade_operators/crypto_policy/constants.py @@ -8,10 +8,9 @@ from ocp_resources.network_addons_config import NetworkAddonsConfig from ocp_resources.ssp import SSP -from tests.install_upgrade_operators.constants import KEY_PATH_SEPARATOR +from tests.install_upgrade_operators.constants import KEY_PATH_SEPARATOR, MANAGED_CRS_LIST from utilities.constants import TLS_CUSTOM_POLICY, TLS_OLD_POLICY -MANAGED_CRS_LIST = [KubeVirt, CDI, NetworkAddonsConfig, SSP] MANAGED_CRS_LIST_WITH_AAQ = [*MANAGED_CRS_LIST, AAQ] TLS_MODERN_POLICY = "modern" diff --git a/tests/install_upgrade_operators/hco_enablement_golden_image_updates/conftest.py b/tests/install_upgrade_operators/hco_enablement_golden_image_updates/conftest.py index 99a74f2b13..2465b0a283 100644 --- a/tests/install_upgrade_operators/hco_enablement_golden_image_updates/conftest.py +++ b/tests/install_upgrade_operators/hco_enablement_golden_image_updates/conftest.py @@ -1,18 +1,25 @@ import pytest +from ocp_resources.data_import_cron import DataImportCron +from ocp_resources.data_source import DataSource from ocp_resources.image_stream import ImageStream from ocp_resources.pod import Pod from ocp_utilities.infra import get_pods_by_name_prefix +from tests.install_upgrade_operators.constants import ENABLE_MULTI_ARCH_BOOT_IMAGE_IMPORT from tests.install_upgrade_operators.hco_enablement_golden_image_updates.utils import ( + COMMON_TEMPLATE, CUSTOM_TEMPLATE, HCO_CR_DATA_IMPORT_SCHEDULE_KEY, get_modifed_common_template_names, get_random_minutes_hours_fields_from_data_import_schedule, get_templates_by_type_from_hco_status, + get_templates_resources_names_dict, ) from utilities.constants import ( COMMON_TEMPLATES_KEY_NAME, + FEATURE_GATES, HCO_OPERATOR, + KUBERNETES_ARCH_LABEL, SSP_CR_COMMON_TEMPLATES_LIST_KEY_NAME, ) from utilities.ssp import get_ssp_resource @@ -113,3 +120,50 @@ def ssp_spec_templates_scope_function(ssp_resource_scope_function): @pytest.fixture(scope="session") def common_templates_scope_session(hyperconverged_status_scope_session): return hyperconverged_status_scope_session[SSP_CR_COMMON_TEMPLATES_LIST_KEY_NAME] + + +@pytest.fixture(scope="session") +def worker_architectures(schedulable_nodes): + return {node.labels[KUBERNETES_ARCH_LABEL] for node in schedulable_nodes} + + +@pytest.fixture(scope="class") +def default_common_template_hco_status(hyperconverged_status_templates_scope_class): + return get_templates_by_type_from_hco_status( + hco_status_templates=hyperconverged_status_templates_scope_class, template_type=COMMON_TEMPLATE + ) + + +@pytest.fixture(scope="class") +def default_common_templates_related_resources( + default_common_template_hco_status, + hyperconverged_resource_scope_class, + worker_architectures, +): + """Return expected golden image resource names for the current cluster state. + + When enableMultiArchBootImageImport is enabled: + - DataImportCrons: arch-specific only (base names are replaced) + - DataSources: both arch-specific and agnostic pointers + - ImageStreams: always base names + When disabled: all base names. + """ + base_resources = get_templates_resources_names_dict(templates=default_common_template_hco_status) + + feature_gate_enabled = hyperconverged_resource_scope_class.instance.spec.get(FEATURE_GATES, {}).get( + ENABLE_MULTI_ARCH_BOOT_IMAGE_IMPORT, False + ) + + if not feature_gate_enabled: + return base_resources + + result = {} + for kind, base_names in base_resources.items(): + arch_names = {f"{name}-{arch}" for name in base_names for arch in worker_architectures} + if kind == DataImportCron.kind: + result[kind] = arch_names + elif kind == DataSource.kind: + result[kind] = base_names | arch_names + else: + result[kind] = base_names + return result diff --git a/tests/install_upgrade_operators/hco_enablement_golden_image_updates/multiarch/conftest.py b/tests/install_upgrade_operators/hco_enablement_golden_image_updates/multiarch/conftest.py new file mode 100644 index 0000000000..0a9a27ac44 --- /dev/null +++ b/tests/install_upgrade_operators/hco_enablement_golden_image_updates/multiarch/conftest.py @@ -0,0 +1,114 @@ +import logging + +import pytest +from ocp_resources.cdi import CDI +from ocp_resources.kubevirt import KubeVirt +from ocp_resources.ssp import SSP + +from tests.install_upgrade_operators.constants import ENABLE_MULTI_ARCH_BOOT_IMAGE_IMPORT, MANAGED_CRS_LIST +from tests.install_upgrade_operators.hco_enablement_golden_image_updates.multiarch.utils import ( + CUSTOM_MULTIARCH_DATASOURCE_NAME, + get_control_plane_architecture, + get_no_arch_annotation_template, + get_unsupported_arch_template, +) +from utilities.constants import ( + FEATURE_GATES, + KUBERNETES_ARCH_LABEL, +) +from utilities.hco import ( + ResourceEditorValidateHCOReconcile, + update_hco_templates_spec, +) + +LOGGER = logging.getLogger(__name__) + +_MULTIARCH_MANAGED_CRS = [SSP, KubeVirt, CDI] + + +@pytest.fixture(scope="class") +def disabled_multiarch_feature_gate(hyperconverged_resource_scope_class): + with ResourceEditorValidateHCOReconcile( + patches={ + hyperconverged_resource_scope_class: {"spec": {FEATURE_GATES: {ENABLE_MULTI_ARCH_BOOT_IMAGE_IMPORT: False}}} + }, + list_resource_reconcile=_MULTIARCH_MANAGED_CRS, + wait_for_reconcile_post_update=True, + ): + yield + + +@pytest.fixture(scope="class") +def enabled_multiarch_feature_gate(hyperconverged_resource_scope_class): + feature_gates = hyperconverged_resource_scope_class.instance.spec.get(FEATURE_GATES, {}) + if feature_gates.get(ENABLE_MULTI_ARCH_BOOT_IMAGE_IMPORT): + yield + else: + with ResourceEditorValidateHCOReconcile( + patches={ + hyperconverged_resource_scope_class: { + "spec": {FEATURE_GATES: {ENABLE_MULTI_ARCH_BOOT_IMAGE_IMPORT: True}} + } + }, + list_resource_reconcile=_MULTIARCH_MANAGED_CRS, + wait_for_reconcile_post_update=True, + ): + yield + + +@pytest.fixture(scope="class") +def control_plane_architecture(control_plane_nodes): + return get_control_plane_architecture(control_plane_nodes=control_plane_nodes) + + +@pytest.fixture() +def single_arch_node_placement(hyperconverged_resource_scope_function, worker_architectures): + single_arch = sorted(worker_architectures)[0] + LOGGER.info(f"Restricting workloads nodePlacement to single architecture: {single_arch}") + placement = {"nodePlacement": {"nodeSelector": {KUBERNETES_ARCH_LABEL: single_arch}}} + with ResourceEditorValidateHCOReconcile( + patches={hyperconverged_resource_scope_function: {"spec": {"workloads": placement}}}, + list_resource_reconcile=MANAGED_CRS_LIST, + wait_for_reconcile_post_update=True, + ): + yield + + +@pytest.fixture() +def hco_with_custom_unsupported_arch_template( + admin_client, + hco_namespace, + hyperconverged_resource_scope_function, + hyperconverged_status_templates_scope_function, + golden_images_namespace, +): + yield from update_hco_templates_spec( + admin_client=admin_client, + hco_namespace=hco_namespace, + hyperconverged_resource=hyperconverged_resource_scope_function, + updated_template=get_unsupported_arch_template( + common_templates=hyperconverged_status_templates_scope_function, + ), + custom_datasource_name=CUSTOM_MULTIARCH_DATASOURCE_NAME, + golden_images_namespace=golden_images_namespace, + ) + + +@pytest.fixture() +def hco_with_custom_no_arch_annotation_template( + admin_client, + hco_namespace, + hyperconverged_resource_scope_function, + hyperconverged_status_templates_scope_function, + golden_images_namespace, +): + yield from update_hco_templates_spec( + admin_client=admin_client, + hco_namespace=hco_namespace, + hyperconverged_resource=hyperconverged_resource_scope_function, + updated_template=get_no_arch_annotation_template( + common_templates=hyperconverged_status_templates_scope_function, + ), + custom_datasource_name=CUSTOM_MULTIARCH_DATASOURCE_NAME, + golden_images_namespace=golden_images_namespace, + ) diff --git a/tests/install_upgrade_operators/hco_enablement_golden_image_updates/multiarch/test_multiarch_golden_images_support.py b/tests/install_upgrade_operators/hco_enablement_golden_image_updates/multiarch/test_multiarch_golden_images_support.py index a57008b2e7..1ba30de894 100644 --- a/tests/install_upgrade_operators/hco_enablement_golden_image_updates/multiarch/test_multiarch_golden_images_support.py +++ b/tests/install_upgrade_operators/hco_enablement_golden_image_updates/multiarch/test_multiarch_golden_images_support.py @@ -8,17 +8,32 @@ - Multi-architecture cluster with AMD64 and ARM64 worker nodes - "enableMultiArchBootImageImport" feature gate enabled in HCO CR - Prometheus is installed and running - -Markers: - - multiarch - - post_upgrade """ +import logging + import pytest +from ocp_resources.data_import_cron import DataImportCron +from ocp_resources.data_source import DataSource + +from tests.install_upgrade_operators.hco_enablement_golden_image_updates.multiarch.utils import ( + CUSTOM_MULTIARCH_DATASOURCE_NAME, + CUSTOM_NO_ARCH_ANNOTATION_CRON_NAME, + CUSTOM_UNSUPPORTED_ARCH_CRON_NAME, + KUBEVIRT_HCO_DATAIMPORTCRONTEMPLATE_WITH_ARCHITECTURE_ANNOTATION_QUERY, + KUBEVIRT_HCO_DATAIMPORTCRONTEMPLATE_WITH_SUPPORTED_ARCHITECTURES_QUERY, + KUBEVIRT_HCO_MULTI_ARCH_BOOT_IMAGES_ENABLED, +) +from tests.install_upgrade_operators.hco_enablement_golden_image_updates.utils import verify_resource_in_ns +from utilities.jira import is_jira_open +from utilities.monitoring import validate_metrics_value -__test__ = False +LOGGER = logging.getLogger(__name__) +pytestmark = pytest.mark.multiarch + +@pytest.mark.usefixtures("disabled_multiarch_feature_gate") class TestDisabledMultiarchGoldenImagesSupport: """ Tests for boot source state and misconfiguration metrics when @@ -30,26 +45,47 @@ class TestDisabledMultiarchGoldenImagesSupport: """ @pytest.mark.polarion("CNV-15977") - def test_only_architecture_agnostic_golden_image_resources_exist(self): + def test_only_architecture_agnostic_golden_image_resources_exist( + self, + admin_client, + golden_images_namespace, + worker_architectures, + subtests, + ): """ Test that only architecture-agnostic golden image resources exist after disabling multi-architecture golden images support. - Parametrize: - - resource_type: - - DataImportCron - - DataSource - Steps: - 1. List resources of the parametrized type in the golden images namespace. - 2. Verify arch-suffix resources are not present. + 1. List DataImportCrons and DataSources in the golden images namespace. + 2. Verify no resources have architecture suffix. Expected: - - No resources exist with architecture suffix. + - No DataImportCron or DataSource resources exist with architecture suffix. """ + for resource_type in (DataImportCron, DataSource): + if resource_type is DataSource and is_jira_open("CNV-68996"): + LOGGER.warning("CNV-68996: arch-specific DataSources not cleaned up after disabling multiarch") + continue + with subtests.test(msg=resource_type.kind): + resources = list(resource_type.get(client=admin_client, namespace=golden_images_namespace.name)) + arch_specific = [ + resource.name + for resource in resources + if any(resource.name.endswith(f"-{arch}") for arch in worker_architectures) + ] + assert not arch_specific, ( + f"Architecture-specific {resource_type.kind} resources found when multiarch is disabled: " + f"{arch_specific}" + ) @pytest.mark.polarion("CNV-15978") - def test_architecture_agnostic_data_sources_rollback(self): + def test_architecture_agnostic_data_sources_rollback( + self, + admin_client, + golden_images_namespace, + default_common_templates_related_resources, + ): """ Test that architecture-agnostic (pointer) DataSources remain available after disabling multi-architecture golden images support, and pointing to a pvc/snapshot source. @@ -61,9 +97,26 @@ def test_architecture_agnostic_data_sources_rollback(self): Expected: - Architecture-agnostic DataSources reference a pvc/snapshot source. """ + verify_resource_in_ns( + expected_resource_names=default_common_templates_related_resources[DataSource.kind], + namespace=golden_images_namespace.name, + client=admin_client, + resource_type=DataSource, + ready_condition=DataSource.Condition.READY, + ) + for ds_name in default_common_templates_related_resources[DataSource.kind]: + data_source = DataSource( + name=ds_name, + namespace=golden_images_namespace.name, + client=admin_client, + ) + source = data_source.instance.spec.source + assert source.get("pvc") or source.get("snapshot"), ( + f"DataSource {ds_name} does not reference a pvc/snapshot source: {source}" + ) @pytest.mark.polarion("CNV-15979") - def test_kubevirt_hco_multi_arch_boot_images_enabled_metric(self): + def test_kubevirt_hco_multi_arch_boot_images_enabled_metric(self, prometheus): """ Test that the metric is indicating that multi-arch golden images support is disabled on a multiarch cluster. @@ -74,12 +127,21 @@ def test_kubevirt_hco_multi_arch_boot_images_enabled_metric(self): Expected: - Metric value is 0. """ + validate_metrics_value( + prometheus=prometheus, + metric_name=KUBEVIRT_HCO_MULTI_ARCH_BOOT_IMAGES_ENABLED, + expected_value="0", + ) + @pytest.mark.usefixtures("single_arch_node_placement") @pytest.mark.polarion("CNV-15980") - def test_kubevirt_hco_multi_arch_boot_images_enabled_metric_single_arch_node_placement(self): + def test_kubevirt_hco_multi_arch_boot_images_enabled_metric_single_arch_node_placement( + self, + prometheus, + ): """ - Test that the metric is indicating that multi-arch support is enabled - when nodePlacement restricts workloads to a single architecture. + Test that the metric is not emitted when nodePlacement restricts + workloads to a single architecture. Preconditions: - nodePlacement restricts workloads to a single architecture in HCO CR. @@ -88,10 +150,16 @@ def test_kubevirt_hco_multi_arch_boot_images_enabled_metric_single_arch_node_pla 1. Query the metric. Expected: - - Metric value is 1. + - Metric is not emitted. """ + validate_metrics_value( + prometheus=prometheus, + metric_name=KUBEVIRT_HCO_MULTI_ARCH_BOOT_IMAGES_ENABLED, + expected_value=0, + ) +@pytest.mark.usefixtures("enabled_multiarch_feature_gate") class TestEnabledMultiarchGoldenImagesSupport: """ Tests for architecture-specific golden image boot sources availability @@ -101,8 +169,21 @@ class TestEnabledMultiarchGoldenImagesSupport: - "enableMultiArchBootImageImport" feature gate enabled in HCO CR """ - @pytest.mark.polarion("CNV-15981") - def test_architecture_specific_golden_image_resources(self): + @pytest.mark.parametrize( + "resource_type, expected_condition", + [ + pytest.param(DataImportCron, DataImportCron.Condition.UP_TO_DATE, marks=pytest.mark.polarion("CNV-15981")), + pytest.param(DataSource, DataSource.Condition.READY, marks=pytest.mark.polarion("CNV-15982")), + ], + ) + def test_architecture_specific_golden_image_resources( + self, + admin_client, + golden_images_namespace, + default_common_templates_related_resources, + resource_type, + expected_condition, + ): """ Test that architecture-specific golden image resources are created for each common DataImportCronTemplate and each supported cluster architecture. @@ -120,9 +201,22 @@ def test_architecture_specific_golden_image_resources(self): - Architecture-specific golden image resources exist for each supported architecture matching the workers architectures and in expected condition. """ - - @pytest.mark.polarion("CNV-15982") - def test_architecture_agnostic_data_sources(self): + verify_resource_in_ns( + expected_resource_names=default_common_templates_related_resources[resource_type.kind], + namespace=golden_images_namespace.name, + client=admin_client, + resource_type=resource_type, + ready_condition=expected_condition, + ) + + @pytest.mark.polarion("CNV-16020") + def test_architecture_agnostic_data_sources( + self, + admin_client, + golden_images_namespace, + default_common_template_hco_status, + control_plane_architecture, + ): """ Test that architecture-agnostic (pointer) DataSources are referencing the default architecture-specific DataSource. @@ -135,8 +229,29 @@ def test_architecture_agnostic_data_sources(self): - DataSources in ready condition and referencing the control-plane architecture-specific DataSource. """ - - + base_ds_names = {template["spec"]["managedDataSource"] for template in default_common_template_hco_status} + verify_resource_in_ns( + expected_resource_names=base_ds_names, + namespace=golden_images_namespace.name, + client=admin_client, + resource_type=DataSource, + ready_condition=DataSource.Condition.READY, + ) + for ds_name in base_ds_names: + data_source = DataSource( + name=ds_name, + namespace=golden_images_namespace.name, + client=admin_client, + ) + expected_arch_ds_prefix = f"{ds_name}-{control_plane_architecture}" + assert data_source.source.name.startswith(expected_arch_ds_prefix), ( + f"DataSource {ds_name} does not reference a control-plane " + f"architecture-specific DataSource (expected prefix: {expected_arch_ds_prefix}). " + f"Actual source: {data_source.source.name}" + ) + + +@pytest.mark.usefixtures("enabled_multiarch_feature_gate") class TestMultiarchGoldenImageAnnotationMetrics: """ Tests for misconfiguration metrics on golden image annotation issues @@ -146,8 +261,12 @@ class TestMultiarchGoldenImageAnnotationMetrics: - "enableMultiArchBootImageImport" feature gate enabled in HCO CR """ + @pytest.mark.usefixtures("hco_with_custom_unsupported_arch_template") @pytest.mark.polarion("CNV-15983") - def test_kubevirt_hco_dataimportcrontemplate_with_supported_architectures_metric(self): + def test_kubevirt_hco_dataimportcrontemplate_with_supported_architectures_metric( + self, + prometheus, + ): """ [NEGATIVE] Test that a misconfiguration metric is reported when a golden image is annotated with an architecture not supported by the cluster. @@ -162,9 +281,21 @@ def test_kubevirt_hco_dataimportcrontemplate_with_supported_architectures_metric Expected: - Metric value is 0. """ - + validate_metrics_value( + prometheus=prometheus, + metric_name=KUBEVIRT_HCO_DATAIMPORTCRONTEMPLATE_WITH_SUPPORTED_ARCHITECTURES_QUERY.format( + cron_name=CUSTOM_UNSUPPORTED_ARCH_CRON_NAME, + ds_name=CUSTOM_MULTIARCH_DATASOURCE_NAME, + ), + expected_value="0", + ) + + @pytest.mark.usefixtures("hco_with_custom_no_arch_annotation_template") @pytest.mark.polarion("CNV-15984") - def test_kubevirt_hco_dataimportcrontemplate_with_architecture_annotation_metric(self): + def test_kubevirt_hco_dataimportcrontemplate_with_architecture_annotation_metric( + self, + prometheus, + ): """ [NEGATIVE] Test that a misconfiguration metric is reported when a golden image lacks an architecture annotation on a multi-architecture cluster. @@ -179,3 +310,11 @@ def test_kubevirt_hco_dataimportcrontemplate_with_architecture_annotation_metric Expected: - Metric value is 0. """ + validate_metrics_value( + prometheus=prometheus, + metric_name=KUBEVIRT_HCO_DATAIMPORTCRONTEMPLATE_WITH_ARCHITECTURE_ANNOTATION_QUERY.format( + cron_name=CUSTOM_NO_ARCH_ANNOTATION_CRON_NAME, + ds_name=CUSTOM_MULTIARCH_DATASOURCE_NAME, + ), + expected_value="0", + ) diff --git a/tests/install_upgrade_operators/hco_enablement_golden_image_updates/multiarch/utils.py b/tests/install_upgrade_operators/hco_enablement_golden_image_updates/multiarch/utils.py new file mode 100644 index 0000000000..c63861007e --- /dev/null +++ b/tests/install_upgrade_operators/hco_enablement_golden_image_updates/multiarch/utils.py @@ -0,0 +1,73 @@ +import logging +from copy import deepcopy +from typing import Any + +from ocp_resources.node import Node + +from utilities.constants import KUBERNETES_ARCH_LABEL + +LOGGER = logging.getLogger(__name__) + +KUBEVIRT_HCO_MULTI_ARCH_BOOT_IMAGES_ENABLED = "kubevirt_hco_multi_arch_boot_images_enabled" +MULTIARCH_DICT_ANNOTATION = "ssp.kubevirt.io/dict.architectures" + +CUSTOM_MULTIARCH_DATASOURCE_NAME = "custom-multiarch-datasource" +CUSTOM_UNSUPPORTED_ARCH_CRON_NAME = "custom-unsupported-arch-cron" +CUSTOM_NO_ARCH_ANNOTATION_CRON_NAME = "custom-no-arch-annotation-cron" + +KUBEVIRT_HCO_DATAIMPORTCRONTEMPLATE_WITH_SUPPORTED_ARCHITECTURES_QUERY = ( + "kubevirt_hco_dataimportcrontemplate_with_supported_architectures" + "{{data_import_cron_name='{cron_name}', managed_data_source_name='{ds_name}'}}" +) +KUBEVIRT_HCO_DATAIMPORTCRONTEMPLATE_WITH_ARCHITECTURE_ANNOTATION_QUERY = ( + "kubevirt_hco_dataimportcrontemplate_with_architecture_annotation" + "{{data_import_cron_name='{cron_name}', managed_data_source_name='{ds_name}'}}" +) + + +def get_control_plane_architecture(control_plane_nodes: list[Node]) -> str: + """Return the architecture of the first control plane node. + + Assumes all control plane nodes share the same architecture. + + Args: + control_plane_nodes: List of control plane Node objects. + + Returns: + str: Architecture label value (e.g. "amd64", "arm64"). + """ + return control_plane_nodes[0].labels[KUBERNETES_ARCH_LABEL] + + +def get_modified_data_import_cron_template( + common_templates: list[dict[str, Any]], + name: str, + managed_data_source: str, + annotations: dict[str, str] | None = None, +) -> dict[str, Any]: + template = deepcopy(common_templates[0]) + del template["status"] + template["metadata"]["name"] = name + template["spec"]["managedDataSource"] = managed_data_source + if annotations is not None: + template["metadata"].setdefault("annotations", {}).update(annotations) + return template + + +def get_unsupported_arch_template(common_templates: list[dict[str, Any]]) -> dict[str, Any]: + return get_modified_data_import_cron_template( + common_templates=common_templates, + name=CUSTOM_UNSUPPORTED_ARCH_CRON_NAME, + managed_data_source=CUSTOM_MULTIARCH_DATASOURCE_NAME, + annotations={MULTIARCH_DICT_ANNOTATION: "arm42"}, + ) + + +def get_no_arch_annotation_template(common_templates: list[dict[str, Any]]) -> dict[str, Any]: + template = get_modified_data_import_cron_template( + common_templates=common_templates, + name=CUSTOM_NO_ARCH_ANNOTATION_CRON_NAME, + managed_data_source=CUSTOM_MULTIARCH_DATASOURCE_NAME, + ) + template["metadata"].get("annotations", {}).pop(MULTIARCH_DICT_ANNOTATION, None) + return template diff --git a/tests/install_upgrade_operators/hco_enablement_golden_image_updates/test_custom_golden_images_namespace.py b/tests/install_upgrade_operators/hco_enablement_golden_image_updates/test_custom_golden_images_namespace.py index f062995ae8..15a8c99ada 100644 --- a/tests/install_upgrade_operators/hco_enablement_golden_image_updates/test_custom_golden_images_namespace.py +++ b/tests/install_upgrade_operators/hco_enablement_golden_image_updates/test_custom_golden_images_namespace.py @@ -11,13 +11,10 @@ from ocp_resources.volume_snapshot import VolumeSnapshot from tests.install_upgrade_operators.hco_enablement_golden_image_updates.utils import ( - COMMON_TEMPLATE, - get_templates_by_type_from_hco_status, -) -from utilities.constants import ( - TIMEOUT_3MIN, - TIMEOUT_10MIN, + verify_resource_in_ns, + verify_resource_not_in_ns, ) +from utilities.constants import TIMEOUT_3MIN, TIMEOUT_10MIN from utilities.hco import ( ResourceEditorValidateHCOReconcile, wait_for_hco_conditions, @@ -31,42 +28,6 @@ pytestmark = [pytest.mark.arm64, pytest.mark.s390x] -def get_templates_resources_names_dict(templates): - resource_dict = {} - for template in templates: - image_stream_name = template["spec"]["template"]["spec"]["source"]["registry"].get("imageStream") - if image_stream_name: - resource_dict.setdefault(ImageStream.kind, set()).add(image_stream_name) - resource_dict.setdefault(DataImportCron.kind, set()).add(template["metadata"]["name"]) - resource_dict.setdefault(DataSource.kind, set()).add(template["spec"]["managedDataSource"]) - return resource_dict - - -def verify_resource_not_in_ns(resource_type, namespace, client): - resources = resource_type.get(client=client, namespace=namespace) - resources_names = {resource.name for resource in resources} - assert not resources_names, f"{resource_type.kind} resources shouldn't exist in {namespace}: {resources_names}" - - -def verify_resource_in_ns(expected_resource_names, namespace, client, resource_type, ready_condition=None): - """ - Verify that resources exist in expected_namespace and in ready status. - """ - resources = list(resource_type.get(client=client, namespace=namespace)) - resources_names = {resource.name for resource in resources} - missing_resources_names = expected_resource_names - resources_names - assert not missing_resources_names, f"Missing {resource_type.kind} in {namespace}: {missing_resources_names}" - - if ready_condition: - LOGGER.info(f"Verify that {expected_resource_names} are in {ready_condition} condition") - for resource in resources: - resource.wait_for_condition( - condition=ready_condition, - status=resource.Condition.Status.TRUE, - timeout=TIMEOUT_10MIN, - ) - - def verify_common_template_namespace_updated(common_templates, namespace_name): non_updated_templates = [] for template in common_templates: @@ -83,18 +44,6 @@ def custom_golden_images_namespace(admin_client): yield from create_ns(admin_client=admin_client, name="custom-golden-images-namespace") -@pytest.fixture(scope="class") -def default_common_template_hco_status(hyperconverged_status_templates_scope_class): - return get_templates_by_type_from_hco_status( - hco_status_templates=hyperconverged_status_templates_scope_class, template_type=COMMON_TEMPLATE - ) - - -@pytest.fixture(scope="class") -def default_common_templates_related_resources(default_common_template_hco_status): - return get_templates_resources_names_dict(templates=default_common_template_hco_status) - - @pytest.fixture(scope="class") def updated_common_template_custom_ns( unprivileged_client, diff --git a/tests/install_upgrade_operators/hco_enablement_golden_image_updates/utils.py b/tests/install_upgrade_operators/hco_enablement_golden_image_updates/utils.py index 0f69af33d2..4c94ef9632 100644 --- a/tests/install_upgrade_operators/hco_enablement_golden_image_updates/utils.py +++ b/tests/install_upgrade_operators/hco_enablement_golden_image_updates/utils.py @@ -1,14 +1,19 @@ import logging import re +from typing import Any from kubernetes.dynamic import DynamicClient from kubernetes.dynamic.exceptions import ResourceNotFoundError from ocp_resources.data_import_cron import DataImportCron +from ocp_resources.data_source import DataSource +from ocp_resources.image_stream import ImageStream +from ocp_resources.resource import Resource from tests.install_upgrade_operators.constants import CUSTOM_DATASOURCE_NAME from utilities.constants import ( OUTDATED, SSP_CR_COMMON_TEMPLATES_LIST_KEY_NAME, + TIMEOUT_10MIN, WILDCARD_CRON_EXPRESSION, ) @@ -104,3 +109,51 @@ def get_template_dict_by_name(template_name, templates): for template in templates: if template["metadata"]["name"] == template_name: return template + + +def get_templates_resources_names_dict(templates: list[dict[str, Any]]) -> dict[str, set[str]]: + """Extract resource names from HCO DataImportCronTemplates, grouped by kind. + + Returns: + dict[str, set[str]]: Mapping of resource kind to set of names. + Keys: DataImportCron.kind and DataSource.kind are always present; + ImageStream.kind is present only when templates contain an image stream. + """ + resource_dict: dict[str, set[str]] = {} + for template in templates: + image_stream_name = template["spec"]["template"]["spec"]["source"]["registry"].get("imageStream") + if image_stream_name: + resource_dict.setdefault(ImageStream.kind, set()).add(image_stream_name) + resource_dict.setdefault(DataImportCron.kind, set()).add(template["metadata"]["name"]) + resource_dict.setdefault(DataSource.kind, set()).add(template["spec"]["managedDataSource"]) + return resource_dict + + +def verify_resource_not_in_ns(resource_type: type[Resource], namespace: str, client: DynamicClient) -> None: + resources = resource_type.get(client=client, namespace=namespace) + resources_names = {resource.name for resource in resources} + assert not resources_names, f"{resource_type.kind} resources shouldn't exist in {namespace}: {resources_names}" + + +def verify_resource_in_ns( + expected_resource_names: set[str], + namespace: str, + client: DynamicClient, + resource_type: type[Resource], + ready_condition: str | None = None, +) -> None: + """Verify that resources exist in namespace and optionally in ready condition.""" + resources = list(resource_type.get(client=client, namespace=namespace)) + resources_names = {resource.name for resource in resources} + missing_resources_names = expected_resource_names - resources_names + assert not missing_resources_names, f"Missing {resource_type.kind} in {namespace}: {missing_resources_names}" + + if ready_condition: + LOGGER.info(f"Verify that {expected_resource_names} are in {ready_condition} condition") + for resource in resources: + if resource.name in expected_resource_names: + resource.wait_for_condition( + condition=ready_condition, + status=resource.Condition.Status.TRUE, + timeout=TIMEOUT_10MIN, + ) diff --git a/tests/observability/metrics/conftest.py b/tests/observability/metrics/conftest.py index 4330d744dd..cdcd71a50b 100644 --- a/tests/observability/metrics/conftest.py +++ b/tests/observability/metrics/conftest.py @@ -36,7 +36,6 @@ network_packets_received, vnic_info_from_vm_or_vmi, ) -from tests.observability.utils import validate_metrics_value from tests.utils import create_vms, start_stress_on_vm from utilities import console from utilities.constants import ( @@ -72,7 +71,7 @@ get_pod_by_name_prefix, unique_name, ) -from utilities.monitoring import get_metrics_value +from utilities.monitoring import get_metrics_value, validate_metrics_value from utilities.network import assert_ping_successful, get_ip_from_vm_or_virt_handler_pod, ping from utilities.ssp import verify_ssp_pod_is_running from utilities.storage import ( diff --git a/tests/observability/metrics/test_aaq_metrics.py b/tests/observability/metrics/test_aaq_metrics.py index fc66874cc8..9e56a58dfc 100644 --- a/tests/observability/metrics/test_aaq_metrics.py +++ b/tests/observability/metrics/test_aaq_metrics.py @@ -4,7 +4,7 @@ timestamp_to_seconds, validate_values_from_kube_application_aware_resourcequota_metric, ) -from tests.observability.utils import validate_metrics_value +from utilities.monitoring import validate_metrics_value pytestmark = [ pytest.mark.usefixtures( diff --git a/tests/observability/metrics/test_general_metrics.py b/tests/observability/metrics/test_general_metrics.py index 255fc656bd..a273eb5df1 100644 --- a/tests/observability/metrics/test_general_metrics.py +++ b/tests/observability/metrics/test_general_metrics.py @@ -7,7 +7,7 @@ from libs.net.cluster import is_ipv6_single_stack_cluster from tests.observability.metrics.constants import KUBEVIRT_VMI_NODE_CPU_AFFINITY from tests.observability.metrics.utils import validate_vmi_node_cpu_affinity_with_prometheus -from tests.observability.utils import validate_metrics_value +from utilities.monitoring import validate_metrics_value from utilities.virt import VirtualMachineForTests, fedora_vm_body, running_vm KUBEVIRT_VM_TAG = f"{Resource.ApiGroup.KUBEVIRT_IO}/vm" diff --git a/tests/observability/metrics/test_metrics.py b/tests/observability/metrics/test_metrics.py index 3ffa466e58..2b0a4f8b92 100644 --- a/tests/observability/metrics/test_metrics.py +++ b/tests/observability/metrics/test_metrics.py @@ -9,10 +9,10 @@ assert_vm_metric_virt_handler_pod, compare_kubevirt_vmi_info_metric_with_vm_info, ) -from tests.observability.utils import validate_metrics_value from utilities.constants import ( KUBEVIRT_HCO_HYPERCONVERGED_CR_EXISTS, ) +from utilities.monitoring import validate_metrics_value pytestmark = [pytest.mark.post_upgrade, pytest.mark.sno] diff --git a/tests/observability/metrics/test_migration_metrics.py b/tests/observability/metrics/test_migration_metrics.py index 93f38ad63b..aa6666379f 100644 --- a/tests/observability/metrics/test_migration_metrics.py +++ b/tests/observability/metrics/test_migration_metrics.py @@ -13,7 +13,7 @@ timestamp_to_seconds, wait_for_non_empty_metrics_value, ) -from tests.observability.utils import validate_metrics_value +from utilities.monitoring import validate_metrics_value LOGGER = logging.getLogger(__name__) diff --git a/tests/observability/metrics/test_ssp_metrics.py b/tests/observability/metrics/test_ssp_metrics.py index b770a83206..7153aa867a 100644 --- a/tests/observability/metrics/test_ssp_metrics.py +++ b/tests/observability/metrics/test_ssp_metrics.py @@ -7,12 +7,12 @@ validate_metric_value_with_round_down, validate_metric_value_within_range, ) -from tests.observability.utils import validate_metrics_value from utilities.constants import ( SSP_OPERATOR, VIRT_TEMPLATE_VALIDATOR, ) from utilities.hco import ResourceEditorValidateHCOReconcile +from utilities.monitoring import validate_metrics_value from utilities.virt import VirtualMachineForTests KUBEVIRT_SSP_OPERATOR_UP = "kubevirt_ssp_operator_up" diff --git a/tests/observability/metrics/test_vms_metrics.py b/tests/observability/metrics/test_vms_metrics.py index fb4368c33d..304e2f760e 100644 --- a/tests/observability/metrics/test_vms_metrics.py +++ b/tests/observability/metrics/test_vms_metrics.py @@ -27,7 +27,6 @@ validate_metric_value_greater_than_initial_value, validate_vnic_info, ) -from tests.observability.utils import validate_metrics_value from utilities.constants import ( CAPACITY, MIGRATION_POLICY_VM_LABEL, @@ -38,7 +37,7 @@ USED, ) from utilities.infra import get_node_selector_dict -from utilities.monitoring import get_metrics_value +from utilities.monitoring import get_metrics_value, validate_metrics_value from utilities.virt import VirtualMachineForTests, fedora_vm_body, running_vm LOGGER = logging.getLogger(__name__) diff --git a/tests/observability/upgrade/test_upgrade_observability.py b/tests/observability/upgrade/test_upgrade_observability.py index 8db322118e..03b7c02c86 100644 --- a/tests/observability/upgrade/test_upgrade_observability.py +++ b/tests/observability/upgrade/test_upgrade_observability.py @@ -1,9 +1,9 @@ import pytest from tests.observability.constants import KUBEVIRT_VMI_NUMBER_OF_OUTDATED -from tests.observability.utils import validate_metrics_value from tests.upgrade_params import IUO_UPGRADE_TEST_DEPENDENCY_NODE_ID from utilities.constants import DEPENDENCY_SCOPE_SESSION +from utilities.monitoring import validate_metrics_value @pytest.mark.cnv_upgrade diff --git a/tests/observability/utils.py b/tests/observability/utils.py index 57ecd090bf..96a60e04fd 100644 --- a/tests/observability/utils.py +++ b/tests/observability/utils.py @@ -1,46 +1,13 @@ -import datetime import logging from ocp_utilities.monitoring import Prometheus -from timeout_sampler import TimeoutExpiredError, TimeoutSampler from tests.observability.constants import SSP_COMMON_TEMPLATES_MODIFICATION_REVERTED -from utilities.constants import ( - TIMEOUT_4MIN, - TIMEOUT_15SEC, -) -from utilities.monitoring import get_metrics_value LOGGER = logging.getLogger(__name__) ALLOW_ALERTS_ON_HEALTHY_CLUSTER_LIST = [SSP_COMMON_TEMPLATES_MODIFICATION_REVERTED] -def validate_metrics_value( - prometheus: Prometheus, metric_name: str, expected_value: str, timeout: int = TIMEOUT_4MIN -) -> None: - samples = TimeoutSampler( - wait_timeout=timeout, - sleep=TIMEOUT_15SEC, - func=get_metrics_value, - prometheus=prometheus, - metrics_name=metric_name, - ) - sample = None - comparison_values_log = {} - try: - for sample in samples: - if sample: - comparison_values_log[datetime.datetime.now()] = ( - f"metric: {metric_name} value is: {sample}, the expected value is {expected_value}" - ) - if sample == expected_value: - LOGGER.info("Metrics value matches the expected value!") - return - except TimeoutExpiredError: - LOGGER.error(f"Metrics value: {sample}, expected: {expected_value}, comparison log: {comparison_values_log}") - raise - - def verify_no_listed_alerts_on_cluster(prometheus: Prometheus, alerts_list: list[str]) -> None: """ It gets a list of alerts and verifies that none of them are firing on a cluster. diff --git a/utilities/hco.py b/utilities/hco.py index 9aadf1a0c1..9a17ea51cb 100644 --- a/utilities/hco.py +++ b/utilities/hco.py @@ -515,8 +515,15 @@ def update_hco_templates_spec( custom_datasource_name=None, golden_images_namespace=None, ): + existing_spec_templates = list( + hyperconverged_resource.instance.to_dict()["spec"].get(SSP_CR_COMMON_TEMPLATES_LIST_KEY_NAME, []) + ) with ResourceEditorValidateHCOReconcile( - patches={hyperconverged_resource: {"spec": {SSP_CR_COMMON_TEMPLATES_LIST_KEY_NAME: [updated_template]}}}, + patches={ + hyperconverged_resource: { + "spec": {SSP_CR_COMMON_TEMPLATES_LIST_KEY_NAME: existing_spec_templates + [updated_template]} + } + }, list_resource_reconcile=[SSP, CDI], wait_for_reconcile_post_update=True, ): diff --git a/utilities/monitoring.py b/utilities/monitoring.py index 85616b74d3..606aad903f 100644 --- a/utilities/monitoring.py +++ b/utilities/monitoring.py @@ -1,7 +1,12 @@ +import datetime import logging +from typing import TYPE_CHECKING from timeout_sampler import TimeoutExpiredError, TimeoutSampler +if TYPE_CHECKING: + from ocp_utilities.monitoring import Prometheus + from utilities.constants import ( FIRING_STATE, KUBEVIRT_HYPERCONVERGED_OPERATOR_HEALTH_STATUS, @@ -175,6 +180,43 @@ def get_metrics_value(prometheus, metrics_name): return 0 +def validate_metrics_value( + prometheus: "Prometheus", metric_name: str, expected_value: str | int, timeout: int = TIMEOUT_5MIN +) -> None: + """Wait until the metric matches the expected value. + + Args: + prometheus: Prometheus instance. + metric_name: Name of the metric to query. + expected_value: Expected value to match. Use str for emitted values (e.g. "0"), + int 0 for absent/not-emitted metrics (get_metrics_value returns int 0 when absent). + timeout: Maximum wait time in seconds. + + Raises: + TimeoutExpiredError: If the metric does not match within timeout. + """ + samples = TimeoutSampler( + wait_timeout=timeout, + sleep=TIMEOUT_5SEC, + func=get_metrics_value, + prometheus=prometheus, + metrics_name=metric_name, + ) + sample = None + comparison_values_log = {} + try: + for sample in samples: + comparison_values_log[datetime.datetime.now()] = ( + f"metric: {metric_name} value is: {sample}, the expected value is {expected_value}" + ) + if sample == expected_value: + LOGGER.info(f"Metrics value matches the expected value: {expected_value}") + return + except TimeoutExpiredError: + LOGGER.error(f"Metrics value: {sample}, expected: {expected_value}, comparison log: {comparison_values_log}") + raise + + def wait_for_gauge_metrics_value(prometheus, query, expected_value, timeout=TIMEOUT_5MIN): samples = TimeoutSampler( wait_timeout=timeout, diff --git a/utilities/pytest_utils.py b/utilities/pytest_utils.py index 7b61c1971e..29044f87b9 100644 --- a/utilities/pytest_utils.py +++ b/utilities/pytest_utils.py @@ -598,6 +598,7 @@ def update_cpu_arch_related_config(cpu_arch_option: str) -> None: cpu_arch = cpu_arch_option.split(",") if cpu_arch_option else list(get_cluster_architecture()) if len(cpu_arch) > 1: + py_config["cpu_arch"] = cpu_arch # stored as list for multiarch runs LOGGER.warning("OS matrix generation is not supported for multi-arch runs!") else: arch = cpu_arch[0] diff --git a/utilities/unittests/test_monitoring.py b/utilities/unittests/test_monitoring.py index 98d0812570..088f16bde3 100644 --- a/utilities/unittests/test_monitoring.py +++ b/utilities/unittests/test_monitoring.py @@ -13,6 +13,7 @@ get_metrics_value, validate_alert_cnv_labels, validate_alerts, + validate_metrics_value, wait_for_alert, wait_for_firing_alert_clean_up, wait_for_gauge_metrics_value, @@ -351,6 +352,52 @@ def test_get_metrics_value_no_data(self): assert result == 0 +class TestValidateMetricsValue: + """Test cases for validate_metrics_value function""" + + @patch("utilities.monitoring.TimeoutSampler") + def test_matches_emitted_string_value(self, mock_sampler_cls): + """Test matching when metric is emitted with a string value.""" + mock_sampler_cls.return_value = iter(["0"]) + validate_metrics_value(prometheus=MagicMock(), metric_name="test_metric", expected_value="0") + + @patch("utilities.monitoring.TimeoutSampler") + def test_matches_absent_metric_with_int_zero(self, mock_sampler_cls): + """Test matching absent metric (int 0) with expected_value=0.""" + mock_sampler_cls.return_value = iter([0]) + validate_metrics_value(prometheus=MagicMock(), metric_name="test_metric", expected_value=0) + + @patch("utilities.monitoring.TimeoutSampler") + def test_absent_metric_does_not_match_string_zero(self, mock_sampler_cls): + """Test that absent metric (int 0) does NOT match expected_value='0'.""" + + def raise_after_samples(*args, **kwargs): + def _iter(): + yield 0 + raise TimeoutExpiredError("Timeout") + + return _iter() + + mock_sampler_cls.side_effect = raise_after_samples + with pytest.raises(TimeoutExpiredError): + validate_metrics_value(prometheus=MagicMock(), metric_name="test_metric", expected_value="0") + + @patch("utilities.monitoring.TimeoutSampler") + def test_timeout_when_value_does_not_match(self, mock_sampler_cls): + """Test timeout when metric value never matches expected.""" + + def raise_after_samples(*args, **kwargs): + def _iter(): + yield "5" + raise TimeoutExpiredError("Timeout") + + return _iter() + + mock_sampler_cls.side_effect = raise_after_samples + with pytest.raises(TimeoutExpiredError): + validate_metrics_value(prometheus=MagicMock(), metric_name="test_metric", expected_value="0") + + class TestWaitForGaugeMetricsValue: """Test cases for wait_for_gauge_metrics_value function""" diff --git a/utilities/unittests/test_pytest_utils.py b/utilities/unittests/test_pytest_utils.py index a1f8374bee..00c676db7a 100644 --- a/utilities/unittests/test_pytest_utils.py +++ b/utilities/unittests/test_pytest_utils.py @@ -1890,7 +1890,7 @@ def test_multi_arch_option_logs_warning( mock_logger.warning.assert_called_once_with("OS matrix generation is not supported for multi-arch runs!") mock_generate_common.assert_not_called() mock_generate_instance.assert_not_called() - assert "cpu_arch" not in mock_py_config + assert mock_py_config["cpu_arch"] == ["amd64", "arm64"] @patch("utilities.pytest_utils.generate_instance_type_matrix_dicts") @patch("utilities.pytest_utils.generate_common_template_matrix_dicts") @@ -2115,7 +2115,7 @@ def test_three_arch_option_logs_warning( mock_logger.warning.assert_called_once_with("OS matrix generation is not supported for multi-arch runs!") mock_generate_common.assert_not_called() mock_generate_instance.assert_not_called() - assert "cpu_arch" not in mock_py_config + assert mock_py_config["cpu_arch"] == ["amd64", "arm64", "s390x"] @patch("utilities.pytest_utils.generate_instance_type_matrix_dicts") @patch("utilities.pytest_utils.generate_common_template_matrix_dicts")