# Copyright (c) 2023-2024 Arista Networks, Inc. # Use of this source code is governed by the Apache License 2.0 # that can be found in the LICENSE file. """ test anta.device.py """ from __future__ import annotations from pathlib import Path from typing import Any import pytest from pydantic import ValidationError from yaml import safe_load from anta.catalog import AntaCatalog, AntaTestDefinition from anta.models import AntaTest from anta.tests.interfaces import VerifyL3MTU from anta.tests.mlag import VerifyMlagStatus from anta.tests.software import VerifyEOSVersion from anta.tests.system import ( VerifyAgentLogs, VerifyCoredump, VerifyCPUUtilization, VerifyFileSystemUtilization, VerifyMemoryUtilization, VerifyNTP, VerifyReloadCause, VerifyUptime, ) from tests.lib.utils import generate_test_ids_list from tests.units.test_models import FakeTestWithInput # Test classes used as expected values DATA_DIR: Path = Path(__file__).parent.parent.resolve() / "data" INIT_CATALOG_DATA: list[dict[str, Any]] = [ { "name": "test_catalog", "filename": "test_catalog.yml", "tests": [ (VerifyEOSVersion, VerifyEOSVersion.Input(versions=["4.31.1F"])), ], }, { "name": "test_catalog_with_tags", "filename": "test_catalog_with_tags.yml", "tests": [ ( VerifyUptime, VerifyUptime.Input( minimum=10, filters=VerifyUptime.Input.Filters(tags=["fabric"]), ), ), (VerifyReloadCause, {"filters": {"tags": ["leaf", "spine"]}}), (VerifyCoredump, VerifyCoredump.Input()), (VerifyAgentLogs, AntaTest.Input()), (VerifyCPUUtilization, VerifyCPUUtilization.Input(filters=VerifyCPUUtilization.Input.Filters(tags=["leaf"]))), (VerifyMemoryUtilization, VerifyMemoryUtilization.Input(filters=VerifyMemoryUtilization.Input.Filters(tags=["testdevice"]))), (VerifyFileSystemUtilization, None), (VerifyNTP, {}), (VerifyMlagStatus, None), (VerifyL3MTU, {"mtu": 1500, "filters": {"tags": ["demo"]}}), ], }, { "name": "test_empty_catalog", "filename": "test_empty_catalog.yml", "tests": [], }, ] CATALOG_PARSE_FAIL_DATA: list[dict[str, Any]] = [ { "name": "undefined_tests", "filename": "test_catalog_with_undefined_tests.yml", "error": "FakeTest is not defined in Python module anta.tests.software", }, { "name": "undefined_module", "filename": "test_catalog_with_undefined_module.yml", "error": "Module named anta.tests.undefined cannot be imported", }, { "name": "undefined_module", "filename": "test_catalog_with_undefined_module.yml", "error": "Module named anta.tests.undefined cannot be imported", }, { "name": "syntax_error", "filename": "test_catalog_with_syntax_error_module.yml", "error": "Value error, Module named tests.data.syntax_error cannot be imported. Verify that the module exists and there is no Python syntax issues.", }, { "name": "undefined_module_nested", "filename": "test_catalog_with_undefined_module_nested.yml", "error": "Module named undefined from package anta.tests cannot be imported", }, { "name": "not_a_list", "filename": "test_catalog_not_a_list.yml", "error": "Value error, Syntax error when parsing: True\nIt must be a list of ANTA tests. Check the test catalog.", }, { "name": "test_definition_not_a_dict", "filename": "test_catalog_test_definition_not_a_dict.yml", "error": "Value error, Syntax error when parsing: VerifyEOSVersion\nIt must be a dictionary. Check the test catalog.", }, { "name": "test_definition_multiple_dicts", "filename": "test_catalog_test_definition_multiple_dicts.yml", "error": "Value error, Syntax error when parsing: {'VerifyEOSVersion': {'versions': ['4.25.4M', '4.26.1F']}, " "'VerifyTerminAttrVersion': {'versions': ['4.25.4M']}}\nIt must be a dictionary with a single entry. Check the indentation in the test catalog.", }, {"name": "wrong_type_after_parsing", "filename": "test_catalog_wrong_type.yml", "error": "must be a dict, got str"}, ] CATALOG_FROM_DICT_FAIL_DATA: list[dict[str, Any]] = [ { "name": "undefined_tests", "filename": "test_catalog_with_undefined_tests.yml", "error": "FakeTest is not defined in Python module anta.tests.software", }, { "name": "wrong_type", "filename": "test_catalog_wrong_type.yml", "error": "Wrong input type for catalog data, must be a dict, got str", }, ] CATALOG_FROM_LIST_FAIL_DATA: list[dict[str, Any]] = [ { "name": "wrong_inputs", "tests": [ ( FakeTestWithInput, AntaTest.Input(), ), ], "error": "Test input has type AntaTest.Input but expected type FakeTestWithInput.Input", }, { "name": "no_test", "tests": [(None, None)], "error": "Input should be a subclass of AntaTest", }, { "name": "no_input_when_required", "tests": [(FakeTestWithInput, None)], "error": "Field required", }, { "name": "wrong_input_type", "tests": [(FakeTestWithInput, True)], "error": "Value error, Coud not instantiate inputs as type bool is not valid", }, ] TESTS_SETTER_FAIL_DATA: list[dict[str, Any]] = [ { "name": "not_a_list", "tests": "not_a_list", "error": "The catalog must contain a list of tests", }, { "name": "not_a_list_of_test_definitions", "tests": [42, 43], "error": "A test in the catalog must be an AntaTestDefinition instance", }, ] class Test_AntaCatalog: """ Test for anta.catalog.AntaCatalog """ @pytest.mark.parametrize("catalog_data", INIT_CATALOG_DATA, ids=generate_test_ids_list(INIT_CATALOG_DATA)) def test_parse(self, catalog_data: dict[str, Any]) -> None: """ Instantiate AntaCatalog from a file """ catalog: AntaCatalog = AntaCatalog.parse(str(DATA_DIR / catalog_data["filename"])) assert len(catalog.tests) == len(catalog_data["tests"]) for test_id, (test, inputs) in enumerate(catalog_data["tests"]): assert catalog.tests[test_id].test == test if inputs is not None: if isinstance(inputs, dict): inputs = test.Input(**inputs) assert inputs == catalog.tests[test_id].inputs @pytest.mark.parametrize("catalog_data", INIT_CATALOG_DATA, ids=generate_test_ids_list(INIT_CATALOG_DATA)) def test_from_list(self, catalog_data: dict[str, Any]) -> None: """ Instantiate AntaCatalog from a list """ catalog: AntaCatalog = AntaCatalog.from_list(catalog_data["tests"]) assert len(catalog.tests) == len(catalog_data["tests"]) for test_id, (test, inputs) in enumerate(catalog_data["tests"]): assert catalog.tests[test_id].test == test if inputs is not None: if isinstance(inputs, dict): inputs = test.Input(**inputs) assert inputs == catalog.tests[test_id].inputs @pytest.mark.parametrize("catalog_data", INIT_CATALOG_DATA, ids=generate_test_ids_list(INIT_CATALOG_DATA)) def test_from_dict(self, catalog_data: dict[str, Any]) -> None: """ Instantiate AntaCatalog from a dict """ with open(file=str(DATA_DIR / catalog_data["filename"]), mode="r", encoding="UTF-8") as file: data = safe_load(file) catalog: AntaCatalog = AntaCatalog.from_dict(data) assert len(catalog.tests) == len(catalog_data["tests"]) for test_id, (test, inputs) in enumerate(catalog_data["tests"]): assert catalog.tests[test_id].test == test if inputs is not None: if isinstance(inputs, dict): inputs = test.Input(**inputs) assert inputs == catalog.tests[test_id].inputs @pytest.mark.parametrize("catalog_data", CATALOG_PARSE_FAIL_DATA, ids=generate_test_ids_list(CATALOG_PARSE_FAIL_DATA)) def test_parse_fail(self, catalog_data: dict[str, Any]) -> None: """ Errors when instantiating AntaCatalog from a file """ with pytest.raises((ValidationError, ValueError)) as exec_info: AntaCatalog.parse(str(DATA_DIR / catalog_data["filename"])) if isinstance(exec_info.value, ValidationError): assert catalog_data["error"] in exec_info.value.errors()[0]["msg"] else: assert catalog_data["error"] in str(exec_info) def test_parse_fail_parsing(self, caplog: pytest.LogCaptureFixture) -> None: """ Errors when instantiating AntaCatalog from a file """ with pytest.raises(Exception) as exec_info: AntaCatalog.parse(str(DATA_DIR / "catalog_does_not_exist.yml")) assert "No such file or directory" in str(exec_info) assert len(caplog.record_tuples) >= 1 _, _, message = caplog.record_tuples[0] assert "Unable to parse ANTA Test Catalog file" in message assert "FileNotFoundError ([Errno 2] No such file or directory" in message @pytest.mark.parametrize("catalog_data", CATALOG_FROM_LIST_FAIL_DATA, ids=generate_test_ids_list(CATALOG_FROM_LIST_FAIL_DATA)) def test_from_list_fail(self, catalog_data: dict[str, Any]) -> None: """ Errors when instantiating AntaCatalog from a list of tuples """ with pytest.raises(ValidationError) as exec_info: AntaCatalog.from_list(catalog_data["tests"]) assert catalog_data["error"] in exec_info.value.errors()[0]["msg"] @pytest.mark.parametrize("catalog_data", CATALOG_FROM_DICT_FAIL_DATA, ids=generate_test_ids_list(CATALOG_FROM_DICT_FAIL_DATA)) def test_from_dict_fail(self, catalog_data: dict[str, Any]) -> None: """ Errors when instantiating AntaCatalog from a list of tuples """ with open(file=str(DATA_DIR / catalog_data["filename"]), mode="r", encoding="UTF-8") as file: data = safe_load(file) with pytest.raises((ValidationError, ValueError)) as exec_info: AntaCatalog.from_dict(data) if isinstance(exec_info.value, ValidationError): assert catalog_data["error"] in exec_info.value.errors()[0]["msg"] else: assert catalog_data["error"] in str(exec_info) def test_filename(self) -> None: """ Test filename """ catalog = AntaCatalog(filename="test") assert catalog.filename == Path("test") catalog = AntaCatalog(filename=Path("test")) assert catalog.filename == Path("test") @pytest.mark.parametrize("catalog_data", INIT_CATALOG_DATA, ids=generate_test_ids_list(INIT_CATALOG_DATA)) def test__tests_setter_success(self, catalog_data: dict[str, Any]) -> None: """ Success when setting AntaCatalog.tests from a list of tuples """ catalog = AntaCatalog() catalog.tests = [AntaTestDefinition(test=test, inputs=inputs) for test, inputs in catalog_data["tests"]] assert len(catalog.tests) == len(catalog_data["tests"]) for test_id, (test, inputs) in enumerate(catalog_data["tests"]): assert catalog.tests[test_id].test == test if inputs is not None: if isinstance(inputs, dict): inputs = test.Input(**inputs) assert inputs == catalog.tests[test_id].inputs @pytest.mark.parametrize("catalog_data", TESTS_SETTER_FAIL_DATA, ids=generate_test_ids_list(TESTS_SETTER_FAIL_DATA)) def test__tests_setter_fail(self, catalog_data: dict[str, Any]) -> None: """ Errors when setting AntaCatalog.tests from a list of tuples """ catalog = AntaCatalog() with pytest.raises(ValueError) as exec_info: catalog.tests = catalog_data["tests"] assert catalog_data["error"] in str(exec_info) def test_get_tests_by_tags(self) -> None: """ Test AntaCatalog.test_get_tests_by_tags() """ catalog: AntaCatalog = AntaCatalog.parse(str(DATA_DIR / "test_catalog_with_tags.yml")) tests: list[AntaTestDefinition] = catalog.get_tests_by_tags(tags=["leaf"]) assert len(tests) == 2