# 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.models.py """ # Mypy does not understand AntaTest.Input typing # mypy: disable-error-code=attr-defined from __future__ import annotations import asyncio from typing import Any import pytest from anta.decorators import deprecated_test, skip_on_platforms from anta.device import AntaDevice from anta.models import AntaCommand, AntaTemplate, AntaTest from tests.lib.fixture import DEVICE_HW_MODEL from tests.lib.utils import generate_test_ids class FakeTest(AntaTest): """ANTA test that always succeed""" name = "FakeTest" description = "ANTA test that always succeed" categories = [] commands = [] @AntaTest.anta_test def test(self) -> None: self.result.is_success() class FakeTestWithFailedCommand(AntaTest): """ANTA test with a command that failed""" name = "FakeTestWithFailedCommand" description = "ANTA test with a command that failed" categories = [] commands = [AntaCommand(command="show version", errors=["failed command"])] @AntaTest.anta_test def test(self) -> None: self.result.is_success() class FakeTestWithUnsupportedCommand(AntaTest): """ANTA test with an unsupported command""" name = "FakeTestWithUnsupportedCommand" description = "ANTA test with an unsupported command" categories = [] commands = [AntaCommand(command="show hardware counter drop", errors=["Unavailable command (not supported on this hardware platform) (at token 2: 'counter')"])] @AntaTest.anta_test def test(self) -> None: self.result.is_success() class FakeTestWithInput(AntaTest): """ANTA test with inputs that always succeed""" name = "FakeTestWithInput" description = "ANTA test with inputs that always succeed" categories = [] commands = [] class Input(AntaTest.Input): # pylint: disable=missing-class-docstring string: str @AntaTest.anta_test def test(self) -> None: self.result.is_success(self.inputs.string) class FakeTestWithTemplate(AntaTest): """ANTA test with template that always succeed""" name = "FakeTestWithTemplate" description = "ANTA test with template that always succeed" categories = [] commands = [AntaTemplate(template="show interface {interface}")] class Input(AntaTest.Input): # pylint: disable=missing-class-docstring interface: str def render(self, template: AntaTemplate) -> list[AntaCommand]: return [template.render(interface=self.inputs.interface)] @AntaTest.anta_test def test(self) -> None: self.result.is_success(self.instance_commands[0].command) class FakeTestWithTemplateNoRender(AntaTest): """ANTA test with template that miss the render() method""" name = "FakeTestWithTemplateNoRender" description = "ANTA test with template that miss the render() method" categories = [] commands = [AntaTemplate(template="show interface {interface}")] class Input(AntaTest.Input): # pylint: disable=missing-class-docstring interface: str @AntaTest.anta_test def test(self) -> None: self.result.is_success(self.instance_commands[0].command) class FakeTestWithTemplateBadRender1(AntaTest): """ANTA test with template that raises a AntaTemplateRenderError exception""" name = "FakeTestWithTemplateBadRender" description = "ANTA test with template that raises a AntaTemplateRenderError exception" categories = [] commands = [AntaTemplate(template="show interface {interface}")] class Input(AntaTest.Input): # pylint: disable=missing-class-docstring interface: str def render(self, template: AntaTemplate) -> list[AntaCommand]: return [template.render(wrong_template_param=self.inputs.interface)] @AntaTest.anta_test def test(self) -> None: self.result.is_success(self.instance_commands[0].command) class FakeTestWithTemplateBadRender2(AntaTest): """ANTA test with template that raises an arbitrary exception""" name = "FakeTestWithTemplateBadRender2" description = "ANTA test with template that raises an arbitrary exception" categories = [] commands = [AntaTemplate(template="show interface {interface}")] class Input(AntaTest.Input): # pylint: disable=missing-class-docstring interface: str def render(self, template: AntaTemplate) -> list[AntaCommand]: raise Exception() # pylint: disable=broad-exception-raised @AntaTest.anta_test def test(self) -> None: self.result.is_success(self.instance_commands[0].command) class SkipOnPlatformTest(AntaTest): """ANTA test that is skipped""" name = "SkipOnPlatformTest" description = "ANTA test that is skipped on a specific platform" categories = [] commands = [] @skip_on_platforms([DEVICE_HW_MODEL]) @AntaTest.anta_test def test(self) -> None: self.result.is_success() class UnSkipOnPlatformTest(AntaTest): """ANTA test that is skipped""" name = "UnSkipOnPlatformTest" description = "ANTA test that is skipped on a specific platform" categories = [] commands = [] @skip_on_platforms(["dummy"]) @AntaTest.anta_test def test(self) -> None: self.result.is_success() class SkipOnPlatformTestWithInput(AntaTest): """ANTA test skipped on platforms but with Input""" name = "SkipOnPlatformTestWithInput" description = "ANTA test skipped on platforms but with Input" categories = [] commands = [] class Input(AntaTest.Input): # pylint: disable=missing-class-docstring string: str @skip_on_platforms([DEVICE_HW_MODEL]) @AntaTest.anta_test def test(self) -> None: self.result.is_success(self.inputs.string) class DeprecatedTestWithoutNewTest(AntaTest): """ANTA test that is deprecated without new test""" name = "DeprecatedTestWitouthNewTest" description = "ANTA test that is deprecated without new test" categories = [] commands = [] @deprecated_test() @AntaTest.anta_test def test(self) -> None: self.result.is_success() class DeprecatedTestWithNewTest(AntaTest): """ANTA test that is deprecated with new test.""" name = "DeprecatedTestWithNewTest" description = "ANTA deprecated test with New Test" categories = [] commands = [] @deprecated_test(new_tests=["NewTest"]) @AntaTest.anta_test def test(self) -> None: self.result.is_success() ANTATEST_DATA: list[dict[str, Any]] = [ {"name": "no input", "test": FakeTest, "inputs": None, "expected": {"__init__": {"result": "unset"}, "test": {"result": "success"}}}, { "name": "extra input", "test": FakeTest, "inputs": {"string": "culpa! veniam quas quas veniam molestias, esse"}, "expected": {"__init__": {"result": "error", "messages": ["Extra inputs are not permitted"]}, "test": {"result": "error"}}, }, { "name": "no input", "test": FakeTestWithInput, "inputs": None, "expected": {"__init__": {"result": "error", "messages": ["Field required"]}, "test": {"result": "error"}}, }, { "name": "wrong input type", "test": FakeTestWithInput, "inputs": {"string": 1}, "expected": {"__init__": {"result": "error", "messages": ["Input should be a valid string"]}, "test": {"result": "error"}}, }, { "name": "good input", "test": FakeTestWithInput, "inputs": {"string": "culpa! veniam quas quas veniam molestias, esse"}, "expected": {"__init__": {"result": "unset"}, "test": {"result": "success", "messages": ["culpa! veniam quas quas veniam molestias, esse"]}}, }, { "name": "good input", "test": FakeTestWithTemplate, "inputs": {"interface": "Ethernet1"}, "expected": {"__init__": {"result": "unset"}, "test": {"result": "success", "messages": ["show interface Ethernet1"]}}, }, { "name": "wrong input type", "test": FakeTestWithTemplate, "inputs": {"interface": 1}, "expected": {"__init__": {"result": "error", "messages": ["Input should be a valid string"]}, "test": {"result": "error"}}, }, { "name": "wrong render definition", "test": FakeTestWithTemplateNoRender, "inputs": {"interface": "Ethernet1"}, "expected": { "__init__": { "result": "error", "messages": ["AntaTemplate are provided but render() method has not been implemented for tests.units.test_models.FakeTestWithTemplateNoRender"], }, "test": {"result": "error"}, }, }, { "name": "AntaTemplateRenderError", "test": FakeTestWithTemplateBadRender1, "inputs": {"interface": "Ethernet1"}, "expected": { "__init__": { "result": "error", "messages": ["Cannot render template {template='show interface {interface}' version='latest' revision=None ofmt='json' use_cache=True}"], }, "test": {"result": "error"}, }, }, { "name": "Exception in render()", "test": FakeTestWithTemplateBadRender2, "inputs": {"interface": "Ethernet1"}, "expected": { "__init__": { "result": "error", "messages": ["Exception in tests.units.test_models.FakeTestWithTemplateBadRender2.render(): Exception"], }, "test": {"result": "error"}, }, }, { "name": "unskip on platforms", "test": UnSkipOnPlatformTest, "inputs": None, "expected": { "__init__": {"result": "unset"}, "test": {"result": "success"}, }, }, { "name": "skip on platforms, unset", "test": SkipOnPlatformTest, "inputs": None, "expected": { "__init__": {"result": "unset"}, "test": {"result": "skipped"}, }, }, { "name": "skip on platforms, not unset", "test": SkipOnPlatformTestWithInput, "inputs": None, "expected": {"__init__": {"result": "error", "messages": ["Field required"]}, "test": {"result": "error"}}, }, { "name": "deprecate test without new test", "test": DeprecatedTestWithoutNewTest, "inputs": None, "expected": { "__init__": {"result": "unset"}, "test": {"result": "success"}, }, }, { "name": "deprecate test with new test", "test": DeprecatedTestWithNewTest, "inputs": None, "expected": { "__init__": {"result": "unset"}, "test": {"result": "success"}, }, }, { "name": "failed command", "test": FakeTestWithFailedCommand, "inputs": None, "expected": {"__init__": {"result": "unset"}, "test": {"result": "error", "messages": ["show version has failed: failed command"]}}, }, { "name": "unsupported command", "test": FakeTestWithUnsupportedCommand, "inputs": None, "expected": { "__init__": {"result": "unset"}, "test": {"result": "skipped", "messages": ["Skipped because show hardware counter drop is not supported on pytest"]}, }, }, ] class Test_AntaTest: """ Test for anta.models.AntaTest """ def test__init_subclass__name(self) -> None: """Test __init_subclass__""" # Pylint detects all the classes in here as unused which is on purpose # pylint: disable=unused-variable with pytest.raises(NotImplementedError) as exec_info: class WrongTestNoName(AntaTest): """ANTA test that is missing a name""" description = "ANTA test that is missing a name" categories = [] commands = [] @AntaTest.anta_test def test(self) -> None: self.result.is_success() assert exec_info.value.args[0] == "Class tests.units.test_models.WrongTestNoName is missing required class attribute name" with pytest.raises(NotImplementedError) as exec_info: class WrongTestNoDescription(AntaTest): """ANTA test that is missing a description""" name = "WrongTestNoDescription" categories = [] commands = [] @AntaTest.anta_test def test(self) -> None: self.result.is_success() assert exec_info.value.args[0] == "Class tests.units.test_models.WrongTestNoDescription is missing required class attribute description" with pytest.raises(NotImplementedError) as exec_info: class WrongTestNoCategories(AntaTest): """ANTA test that is missing categories""" name = "WrongTestNoCategories" description = "ANTA test that is missing categories" commands = [] @AntaTest.anta_test def test(self) -> None: self.result.is_success() assert exec_info.value.args[0] == "Class tests.units.test_models.WrongTestNoCategories is missing required class attribute categories" with pytest.raises(NotImplementedError) as exec_info: class WrongTestNoCommands(AntaTest): """ANTA test that is missing commands""" name = "WrongTestNoCommands" description = "ANTA test that is missing commands" categories = [] @AntaTest.anta_test def test(self) -> None: self.result.is_success() assert exec_info.value.args[0] == "Class tests.units.test_models.WrongTestNoCommands is missing required class attribute commands" def _assert_test(self, test: AntaTest, expected: dict[str, Any]) -> None: assert test.result.result == expected["result"] if "messages" in expected: for result_msg, expected_msg in zip(test.result.messages, expected["messages"]): # NOTE: zip(strict=True) has been added in Python 3.10 assert expected_msg in result_msg @pytest.mark.parametrize("data", ANTATEST_DATA, ids=generate_test_ids(ANTATEST_DATA)) def test__init__(self, device: AntaDevice, data: dict[str, Any]) -> None: """Test the AntaTest constructor""" expected = data["expected"]["__init__"] test = data["test"](device, inputs=data["inputs"]) self._assert_test(test, expected) @pytest.mark.parametrize("data", ANTATEST_DATA, ids=generate_test_ids(ANTATEST_DATA)) def test_test(self, device: AntaDevice, data: dict[str, Any]) -> None: """Test the AntaTest.test method""" expected = data["expected"]["test"] test = data["test"](device, inputs=data["inputs"]) asyncio.run(test.test()) self._assert_test(test, expected) ANTATEST_BLACKLIST_DATA = ["reload", "reload --force", "write", "wr mem"] @pytest.mark.parametrize("data", ANTATEST_BLACKLIST_DATA) def test_blacklist(device: AntaDevice, data: str) -> None: """Test for blacklisting function.""" class FakeTestWithBlacklist(AntaTest): """Fake Test for blacklist""" name = "FakeTestWithBlacklist" description = "ANTA test that has blacklisted command" categories = [] commands = [AntaCommand(command=data)] @AntaTest.anta_test def test(self) -> None: self.result.is_success() test_instance = FakeTestWithBlacklist(device) # Run the test() method asyncio.run(test_instance.test()) assert test_instance.result.result == "error"