summaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-14 08:36:44 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-14 08:36:50 +0000
commit7763cc454d686d51bf2e0ccc1f2ccf7d62a0d625 (patch)
treef36d2006dd01bd01a069956741d831d9d5633377 /tests
parentAdding debian version 0.13.0-1. (diff)
downloadanta-7763cc454d686d51bf2e0ccc1f2ccf7d62a0d625.tar.xz
anta-7763cc454d686d51bf2e0ccc1f2ccf7d62a0d625.zip
Merging upstream version 0.14.0.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'tests')
-rw-r--r--tests/__init__.py1
-rw-r--r--tests/conftest.py19
-rw-r--r--tests/data/__init__.py1
-rw-r--r--tests/data/json_data.py15
-rw-r--r--tests/data/test_catalog_with_tags.yml4
-rw-r--r--tests/lib/__init__.py1
-rw-r--r--tests/lib/anta.py14
-rw-r--r--tests/lib/fixture.py160
-rw-r--r--tests/lib/utils.py22
-rw-r--r--tests/units/__init__.py1
-rw-r--r--tests/units/anta_tests/__init__.py1
-rw-r--r--tests/units/anta_tests/routing/__init__.py1
-rw-r--r--tests/units/anta_tests/routing/test_bgp.py1906
-rw-r--r--tests/units/anta_tests/routing/test_generic.py47
-rw-r--r--tests/units/anta_tests/routing/test_ospf.py213
-rw-r--r--tests/units/anta_tests/test_aaa.py105
-rw-r--r--tests/units/anta_tests/test_bfd.py7
-rw-r--r--tests/units/anta_tests/test_configuration.py3
-rw-r--r--tests/units/anta_tests/test_connectivity.py193
-rw-r--r--tests/units/anta_tests/test_field_notices.py104
-rw-r--r--tests/units/anta_tests/test_greent.py22
-rw-r--r--tests/units/anta_tests/test_hardware.py103
-rw-r--r--tests/units/anta_tests/test_interfaces.py933
-rw-r--r--tests/units/anta_tests/test_lanz.py5
-rw-r--r--tests/units/anta_tests/test_logging.py17
-rw-r--r--tests/units/anta_tests/test_mlag.py43
-rw-r--r--tests/units/anta_tests/test_multicast.py19
-rw-r--r--tests/units/anta_tests/test_profiles.py9
-rw-r--r--tests/units/anta_tests/test_ptp.py308
-rw-r--r--tests/units/anta_tests/test_security.py291
-rw-r--r--tests/units/anta_tests/test_services.py5
-rw-r--r--tests/units/anta_tests/test_snmp.py5
-rw-r--r--tests/units/anta_tests/test_software.py11
-rw-r--r--tests/units/anta_tests/test_stp.py57
-rw-r--r--tests/units/anta_tests/test_stun.py176
-rw-r--r--tests/units/anta_tests/test_system.py38
-rw-r--r--tests/units/anta_tests/test_vlan.py5
-rw-r--r--tests/units/anta_tests/test_vxlan.py43
-rw-r--r--tests/units/cli/__init__.py1
-rw-r--r--tests/units/cli/check/__init__.py1
-rw-r--r--tests/units/cli/check/test__init__.py18
-rw-r--r--tests/units/cli/check/test_commands.py11
-rw-r--r--tests/units/cli/debug/__init__.py1
-rw-r--r--tests/units/cli/debug/test__init__.py18
-rw-r--r--tests/units/cli/debug/test_commands.py19
-rw-r--r--tests/units/cli/exec/__init__.py1
-rw-r--r--tests/units/cli/exec/test__init__.py18
-rw-r--r--tests/units/cli/exec/test_commands.py32
-rw-r--r--tests/units/cli/exec/test_utils.py113
-rw-r--r--tests/units/cli/get/__init__.py1
-rw-r--r--tests/units/cli/get/test__init__.py18
-rw-r--r--tests/units/cli/get/test_commands.py112
-rw-r--r--tests/units/cli/get/test_utils.py57
-rw-r--r--tests/units/cli/nrfu/__init__.py1
-rw-r--r--tests/units/cli/nrfu/test__init__.py39
-rw-r--r--tests/units/cli/nrfu/test_commands.py59
-rw-r--r--tests/units/cli/test__init__.py46
-rw-r--r--tests/units/inventory/__init__.py1
-rw-r--r--tests/units/inventory/test_inventory.py19
-rw-r--r--tests/units/inventory/test_models.py76
-rw-r--r--tests/units/reporter/__init__.py1
-rw-r--r--tests/units/reporter/test__init__.py124
-rw-r--r--tests/units/result_manager/__init__.py1
-rw-r--r--tests/units/result_manager/test__init__.py335
-rw-r--r--tests/units/result_manager/test_models.py7
-rw-r--r--tests/units/test_catalog.py128
-rw-r--r--tests/units/test_device.py221
-rw-r--r--tests/units/test_logger.py84
-rw-r--r--tests/units/test_models.py301
-rw-r--r--tests/units/test_runner.py38
-rw-r--r--tests/units/test_tools.py490
-rw-r--r--tests/units/tools/__init__.py3
-rw-r--r--tests/units/tools/test_get_dict_superset.py149
-rw-r--r--tests/units/tools/test_get_item.py72
-rw-r--r--tests/units/tools/test_get_value.py50
-rw-r--r--tests/units/tools/test_misc.py38
-rw-r--r--tests/units/tools/test_utils.py57
77 files changed, 5073 insertions, 2596 deletions
diff --git a/tests/__init__.py b/tests/__init__.py
index e772bee..0a2486a 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -1,3 +1,4 @@
# 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.
+"""Tests for ANTA."""
diff --git a/tests/conftest.py b/tests/conftest.py
index 5a40c24..7aa229c 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,19 +1,15 @@
# 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.
-"""
-conftest.py - used to store anta specific fixtures used for tests
-"""
+"""conftest.py - used to store anta specific fixtures used for tests."""
+
from __future__ import annotations
import logging
-from typing import TYPE_CHECKING, Any
+from typing import Any
import pytest
-if TYPE_CHECKING:
- from pytest import Metafunc
-
# Load fixtures from dedicated file tests/lib/fixture.py
# As well as pytest_asyncio plugin to test co-routines
pytest_plugins = [
@@ -31,8 +27,7 @@ for _ in ("asyncio", "httpx"):
def build_test_id(val: dict[str, Any]) -> str:
- """
- build id for a unit test of an AntaTest subclass
+ """Build id for a unit test of an AntaTest subclass.
{
"name": "meaniful test name",
@@ -43,9 +38,9 @@ def build_test_id(val: dict[str, Any]) -> str:
return f"{val['test'].__module__}.{val['test'].__name__}-{val['name']}"
-def pytest_generate_tests(metafunc: Metafunc) -> None:
- """
- This function is called during test collection.
+def pytest_generate_tests(metafunc: pytest.Metafunc) -> None:
+ """Generate ANTA testts unit tests dynamically during test collection.
+
It will parametrize test cases based on the `DATA` data structure defined in `tests.units.anta_tests` modules.
See `tests/units/anta_tests/README.md` for more information on how to use it.
Test IDs are generated using the `build_test_id` function above.
diff --git a/tests/data/__init__.py b/tests/data/__init__.py
index e772bee..864da68 100644
--- a/tests/data/__init__.py
+++ b/tests/data/__init__.py
@@ -1,3 +1,4 @@
# 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.
+"""Data for unit tests."""
diff --git a/tests/data/json_data.py b/tests/data/json_data.py
index ad2c9ed..5630840 100644
--- a/tests/data/json_data.py
+++ b/tests/data/json_data.py
@@ -2,6 +2,7 @@
# Use of this source code is governed by the Apache License 2.0
# that can be found in the LICENSE file.
# pylint: skip-file
+"""JSON Data for unit tests."""
INVENTORY_MODEL_HOST_VALID = [
{"name": "validIPv4", "input": "1.1.1.1", "expected_result": "valid"},
@@ -92,7 +93,7 @@ INVENTORY_MODEL_VALID = [
"ranges": [
{"start": "10.1.0.1", "end": "10.1.0.10"},
{"start": "10.2.0.1", "end": "10.2.1.10"},
- ]
+ ],
},
"expected_result": "valid",
},
@@ -150,8 +151,8 @@ ANTA_INVENTORY_TESTS_VALID = [
"ranges": [
{"start": "10.0.0.1", "end": "10.0.0.11"},
{"start": "10.0.0.101", "end": "10.0.0.111"},
- ]
- }
+ ],
+ },
},
"expected_result": "valid",
"parameters": {
@@ -197,8 +198,8 @@ ANTA_INVENTORY_TESTS_VALID = [
"ranges": [
{"start": "10.0.0.1", "end": "10.0.0.11", "tags": ["leaf"]},
{"start": "10.0.0.101", "end": "10.0.0.111", "tags": ["spine"]},
- ]
- }
+ ],
+ },
},
"expected_result": "valid",
"parameters": {
@@ -242,8 +243,8 @@ ANTA_INVENTORY_TESTS_INVALID = [
"ranges": [
{"start": "10.0.0.1", "end": "10.0.0.11"},
{"start": "10.0.0.100", "end": "10.0.0.111"},
- ]
- }
+ ],
+ },
},
"expected_result": "invalid",
},
diff --git a/tests/data/test_catalog_with_tags.yml b/tests/data/test_catalog_with_tags.yml
index 0c8f5f6..109781e 100644
--- a/tests/data/test_catalog_with_tags.yml
+++ b/tests/data/test_catalog_with_tags.yml
@@ -4,6 +4,10 @@ anta.tests.system:
minimum: 10
filters:
tags: ['fabric']
+ - VerifyUptime:
+ minimum: 9
+ filters:
+ tags: ['leaf']
- VerifyReloadCause:
filters:
tags: ['leaf', 'spine']
diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py
index e772bee..cd54f3a 100644
--- a/tests/lib/__init__.py
+++ b/tests/lib/__init__.py
@@ -1,3 +1,4 @@
# 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.
+"""Library for ANTA unit tests."""
diff --git a/tests/lib/anta.py b/tests/lib/anta.py
index b97d91d..cabb27b 100644
--- a/tests/lib/anta.py
+++ b/tests/lib/anta.py
@@ -1,20 +1,20 @@
# 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.
-"""
-generic test funciton used to generate unit tests for each AntaTest
-"""
+"""generic test function used to generate unit tests for each AntaTest."""
+
from __future__ import annotations
import asyncio
-from typing import Any
+from typing import TYPE_CHECKING, Any
-from anta.device import AntaDevice
+if TYPE_CHECKING:
+ from anta.device import AntaDevice
def test(device: AntaDevice, data: dict[str, Any]) -> None:
- """
- Generic test function for AntaTest subclass.
+ """Generic test function for AntaTest subclass.
+
See `tests/units/anta_tests/README.md` for more information on how to use it.
"""
# Instantiate the AntaTest subclass
diff --git a/tests/lib/fixture.py b/tests/lib/fixture.py
index 68e9e57..43fb60a 100644
--- a/tests/lib/fixture.py
+++ b/tests/lib/fixture.py
@@ -1,28 +1,32 @@
# 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.
-"""Fixture for Anta Testing"""
+"""Fixture for Anta Testing."""
+
from __future__ import annotations
import logging
import shutil
-from pathlib import Path
-from typing import Any, Callable, Iterator
+from typing import TYPE_CHECKING, Any, Callable
from unittest.mock import patch
import pytest
from click.testing import CliRunner, Result
-from pytest import CaptureFixture
from anta import aioeapi
from anta.cli.console import console
from anta.device import AntaDevice, AsyncEOSDevice
from anta.inventory import AntaInventory
-from anta.models import AntaCommand
from anta.result_manager import ResultManager
from anta.result_manager.models import TestResult
from tests.lib.utils import default_anta_env
+if TYPE_CHECKING:
+ from collections.abc import Iterator
+ from pathlib import Path
+
+ from anta.models import AntaCommand
+
logger = logging.getLogger(__name__)
DEVICE_HW_MODEL = "pytest"
@@ -38,7 +42,11 @@ MOCK_CLI_JSON: dict[str, aioeapi.EapiCommandError | dict[str, Any]] = {
"clear counters": {},
"clear hardware counter drop": {},
"undefined": aioeapi.EapiCommandError(
- passed=[], failed="show version", errors=["Authorization denied for command 'show version'"], errmsg="Invalid command", not_exec=[]
+ passed=[],
+ failed="show version",
+ errors=["Authorization denied for command 'show version'"],
+ errmsg="Invalid command",
+ not_exec=[],
),
}
@@ -50,11 +58,9 @@ MOCK_CLI_TEXT: dict[str, aioeapi.EapiCommandError | str] = {
}
-@pytest.fixture
+@pytest.fixture()
def device(request: pytest.FixtureRequest) -> Iterator[AntaDevice]:
- """
- Returns an AntaDevice instance with mocked abstract method
- """
+ """Return an AntaDevice instance with mocked abstract method."""
def _collect(command: AntaCommand) -> None:
command.output = COMMAND_OUTPUT
@@ -64,22 +70,21 @@ def device(request: pytest.FixtureRequest) -> Iterator[AntaDevice]:
if hasattr(request, "param"):
# Fixture is parametrized indirectly
kwargs.update(request.param)
- with patch.object(AntaDevice, "__abstractmethods__", set()):
- with patch("anta.device.AntaDevice._collect", side_effect=_collect):
- # AntaDevice constructor does not have hw_model argument
- hw_model = kwargs.pop("hw_model")
- dev = AntaDevice(**kwargs) # type: ignore[abstract, arg-type] # pylint: disable=abstract-class-instantiated, unexpected-keyword-arg
- dev.hw_model = hw_model
- yield dev
+ with patch.object(AntaDevice, "__abstractmethods__", set()), patch("anta.device.AntaDevice._collect", side_effect=_collect):
+ # AntaDevice constructor does not have hw_model argument
+ hw_model = kwargs.pop("hw_model")
+ dev = AntaDevice(**kwargs) # type: ignore[abstract, arg-type] # pylint: disable=abstract-class-instantiated, unexpected-keyword-arg
+ dev.hw_model = hw_model
+ yield dev
-@pytest.fixture
+@pytest.fixture()
def test_inventory() -> AntaInventory:
- """
- Return the test_inventory
- """
+ """Return the test_inventory."""
env = default_anta_env()
- assert env["ANTA_INVENTORY"] and env["ANTA_USERNAME"] and env["ANTA_PASSWORD"] is not None
+ assert env["ANTA_INVENTORY"]
+ assert env["ANTA_USERNAME"]
+ assert env["ANTA_PASSWORD"] is not None
return AntaInventory.parse(
filename=env["ANTA_INVENTORY"],
username=env["ANTA_USERNAME"],
@@ -88,34 +93,30 @@ def test_inventory() -> AntaInventory:
# tests.unit.test_device.py fixture
-@pytest.fixture
+@pytest.fixture()
def async_device(request: pytest.FixtureRequest) -> AsyncEOSDevice:
- """
- Returns an AsyncEOSDevice instance
- """
-
- kwargs = {"name": DEVICE_NAME, "host": "42.42.42.42", "username": "anta", "password": "anta"}
+ """Return an AsyncEOSDevice instance."""
+ kwargs = {
+ "name": DEVICE_NAME,
+ "host": "42.42.42.42",
+ "username": "anta",
+ "password": "anta",
+ }
if hasattr(request, "param"):
# Fixture is parametrized indirectly
kwargs.update(request.param)
- dev = AsyncEOSDevice(**kwargs) # type: ignore[arg-type]
- return dev
+ return AsyncEOSDevice(**kwargs) # type: ignore[arg-type]
# tests.units.result_manager fixtures
-@pytest.fixture
+@pytest.fixture()
def test_result_factory(device: AntaDevice) -> Callable[[int], TestResult]:
- """
- Return a anta.result_manager.models.TestResult object
- """
-
+ """Return a anta.result_manager.models.TestResult object."""
# pylint: disable=redefined-outer-name
def _create(index: int = 0) -> TestResult:
- """
- Actual Factory
- """
+ """Actual Factory."""
return TestResult(
name=device.name,
test=f"VerifyTest{index}",
@@ -127,50 +128,39 @@ def test_result_factory(device: AntaDevice) -> Callable[[int], TestResult]:
return _create
-@pytest.fixture
+@pytest.fixture()
def list_result_factory(test_result_factory: Callable[[int], TestResult]) -> Callable[[int], list[TestResult]]:
- """
- Return a list[TestResult] with 'size' TestResult instanciated using the test_result_factory fixture
- """
-
+ """Return a list[TestResult] with 'size' TestResult instantiated using the test_result_factory fixture."""
# pylint: disable=redefined-outer-name
def _factory(size: int = 0) -> list[TestResult]:
- """
- Factory for list[TestResult] entry of size entries
- """
- result: list[TestResult] = []
- for i in range(size):
- result.append(test_result_factory(i))
- return result
+ """Create a factory for list[TestResult] entry of size entries."""
+ return [test_result_factory(i) for i in range(size)]
return _factory
-@pytest.fixture
+@pytest.fixture()
def result_manager_factory(list_result_factory: Callable[[int], list[TestResult]]) -> Callable[[int], ResultManager]:
- """
- Return a ResultManager factory that takes as input a number of tests
- """
-
+ """Return a ResultManager factory that takes as input a number of tests."""
# pylint: disable=redefined-outer-name
def _factory(number: int = 0) -> ResultManager:
- """
- Factory for list[TestResult] entry of size entries
- """
+ """Create a factory for list[TestResult] entry of size entries."""
result_manager = ResultManager()
- result_manager.add_test_results(list_result_factory(number))
+ result_manager.results = list_result_factory(number)
return result_manager
return _factory
# tests.units.cli fixtures
-@pytest.fixture
+@pytest.fixture()
def temp_env(tmp_path: Path) -> dict[str, str | None]:
- """Fixture that create a temporary ANTA inventory that can be overriden
- and returns the corresponding environment variables"""
+ """Fixture that create a temporary ANTA inventory.
+
+ The inventory can be overridden and returns the corresponding environment variables.
+ """
env = default_anta_env()
anta_inventory = str(env["ANTA_INVENTORY"])
temp_inventory = tmp_path / "test_inventory.yml"
@@ -179,16 +169,19 @@ def temp_env(tmp_path: Path) -> dict[str, str | None]:
return env
-@pytest.fixture
-def click_runner(capsys: CaptureFixture[str]) -> Iterator[CliRunner]:
- """
- Convenience fixture to return a click.CliRunner for cli testing
- """
+@pytest.fixture()
+# Disabling C901 - too complex as we like our runner like this
+def click_runner(capsys: pytest.CaptureFixture[str]) -> Iterator[CliRunner]: # noqa: C901
+ """Return a click.CliRunner for cli testing."""
class AntaCliRunner(CliRunner):
- """Override CliRunner to inject specific variables for ANTA"""
+ """Override CliRunner to inject specific variables for ANTA."""
- def invoke(self, *args, **kwargs) -> Result: # type: ignore[no-untyped-def]
+ def invoke(
+ self,
+ *args: Any, # noqa: ANN401
+ **kwargs: Any, # noqa: ANN401
+ ) -> Result:
# Inject default env if not provided
kwargs["env"] = kwargs["env"] if "env" in kwargs else default_anta_env()
# Deterministic terminal width
@@ -198,14 +191,18 @@ def click_runner(capsys: CaptureFixture[str]) -> Iterator[CliRunner]:
# Way to fix https://github.com/pallets/click/issues/824
with capsys.disabled():
result = super().invoke(*args, **kwargs)
- print("--- CLI Output ---")
- print(result.output)
+ # disabling T201 as we want to print here
+ print("--- CLI Output ---") # noqa: T201
+ print(result.output) # noqa: T201
return result
def cli(
- command: str | None = None, commands: list[dict[str, Any]] | None = None, ofmt: str = "json", version: int | str | None = "latest", **kwargs: Any
+ command: str | None = None,
+ commands: list[dict[str, Any]] | None = None,
+ ofmt: str = "json",
+ _version: int | str | None = "latest",
+ **_kwargs: Any, # noqa: ANN401
) -> dict[str, Any] | list[dict[str, Any]]:
- # pylint: disable=unused-argument
def get_output(command: str | dict[str, Any]) -> dict[str, Any]:
if isinstance(command, dict):
command = command["cmd"]
@@ -216,7 +213,7 @@ def click_runner(capsys: CaptureFixture[str]) -> Iterator[CliRunner]:
mock_cli = MOCK_CLI_TEXT
for mock_cmd, output in mock_cli.items():
if command == mock_cmd:
- logger.info(f"Mocking command {mock_cmd}")
+ logger.info("Mocking command %s", mock_cmd)
if isinstance(output, aioeapi.EapiCommandError):
raise output
return output
@@ -226,17 +223,22 @@ def click_runner(capsys: CaptureFixture[str]) -> Iterator[CliRunner]:
res: dict[str, Any] | list[dict[str, Any]]
if command is not None:
- logger.debug(f"Mock input {command}")
+ logger.debug("Mock input %s", command)
res = get_output(command)
if commands is not None:
- logger.debug(f"Mock input {commands}")
+ logger.debug("Mock input %s", commands)
res = list(map(get_output, commands))
- logger.debug(f"Mock output {res}")
+ logger.debug("Mock output %s", res)
return res
# Patch aioeapi methods used by AsyncEOSDevice. See tests/units/test_device.py
- with patch("aioeapi.device.Device.check_connection", return_value=True), patch("aioeapi.device.Device.cli", side_effect=cli), patch("asyncssh.connect"), patch(
- "asyncssh.scp"
+ with (
+ patch("aioeapi.device.Device.check_connection", return_value=True),
+ patch("aioeapi.device.Device.cli", side_effect=cli),
+ patch("asyncssh.connect"),
+ patch(
+ "asyncssh.scp",
+ ),
):
console._color_system = None # pylint: disable=protected-access
yield AntaCliRunner()
diff --git a/tests/lib/utils.py b/tests/lib/utils.py
index 460e014..1255936 100644
--- a/tests/lib/utils.py
+++ b/tests/lib/utils.py
@@ -1,9 +1,8 @@
# 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.
-"""
-tests.lib.utils
-"""
+"""tests.lib.utils."""
+
from __future__ import annotations
from pathlib import Path
@@ -11,22 +10,17 @@ from typing import Any
def generate_test_ids_dict(val: dict[str, Any], key: str = "name") -> str:
- """
- generate_test_ids Helper to generate test ID for parametrize
- """
+ """generate_test_ids Helper to generate test ID for parametrize."""
return val.get(key, "unamed_test")
def generate_test_ids_list(val: list[dict[str, Any]], key: str = "name") -> list[str]:
- """
- generate_test_ids Helper to generate test ID for parametrize
- """
- return [entry[key] if key in entry.keys() else "unamed_test" for entry in val]
+ """generate_test_ids Helper to generate test ID for parametrize."""
+ return [entry.get(key, "unamed_test") for entry in val]
def generate_test_ids(data: list[dict[str, Any]]) -> list[str]:
- """
- build id for a unit test of an AntaTest subclass
+ """Build id for a unit test of an AntaTest subclass.
{
"name": "meaniful test name",
@@ -38,9 +32,7 @@ def generate_test_ids(data: list[dict[str, Any]]) -> list[str]:
def default_anta_env() -> dict[str, str | None]:
- """
- Return a default_anta_environement which can be passed to a cliRunner.invoke method
- """
+ """Return a default_anta_environement which can be passed to a cliRunner.invoke method."""
return {
"ANTA_USERNAME": "anta",
"ANTA_PASSWORD": "formica",
diff --git a/tests/units/__init__.py b/tests/units/__init__.py
index e772bee..6f96a0d 100644
--- a/tests/units/__init__.py
+++ b/tests/units/__init__.py
@@ -1,3 +1,4 @@
# 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.
+"""Unit tests for anta."""
diff --git a/tests/units/anta_tests/__init__.py b/tests/units/anta_tests/__init__.py
index e772bee..8ca0e8c 100644
--- a/tests/units/anta_tests/__init__.py
+++ b/tests/units/anta_tests/__init__.py
@@ -1,3 +1,4 @@
# 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 for anta.tests submodule."""
diff --git a/tests/units/anta_tests/routing/__init__.py b/tests/units/anta_tests/routing/__init__.py
index e772bee..aef1274 100644
--- a/tests/units/anta_tests/routing/__init__.py
+++ b/tests/units/anta_tests/routing/__init__.py
@@ -1,3 +1,4 @@
# 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 for anta.tests.routing submodule."""
diff --git a/tests/units/anta_tests/routing/test_bgp.py b/tests/units/anta_tests/routing/test_bgp.py
index 799f058..31006c5 100644
--- a/tests/units/anta_tests/routing/test_bgp.py
+++ b/tests/units/anta_tests/routing/test_bgp.py
@@ -1,9 +1,8 @@
# 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.
-"""
-Tests for anta.tests.routing.bgp.py
-"""
+"""Tests for anta.tests.routing.bgp.py."""
+
# pylint: disable=C0302
from __future__ import annotations
@@ -11,7 +10,7 @@ from typing import Any
# pylint: disable=C0413
# because of the patch above
-from anta.tests.routing.bgp import ( # noqa: E402
+from anta.tests.routing.bgp import (
VerifyBGPAdvCommunities,
VerifyBGPExchangedRoutes,
VerifyBGPPeerASNCap,
@@ -34,44 +33,100 @@ DATA: list[dict[str, Any]] = [
{
"vrfs": {
"default": {
- "vrf": "default",
- "routerId": "10.1.0.3",
- "asn": "65120",
"peers": {
"10.1.255.0": {
- "description": "DC1-SPINE1_Ethernet1",
- "version": 4,
- "msgReceived": 0,
- "msgSent": 0,
"inMsgQueue": 0,
"outMsgQueue": 0,
- "asn": "65100",
- "prefixAccepted": 0,
- "prefixReceived": 0,
- "upDownTime": 1694266295.098931,
- "underMaintenance": False,
"peerState": "Established",
},
"10.1.255.2": {
+ "inMsgQueue": 0,
+ "outMsgQueue": 0,
+ "peerState": "Established",
+ },
+ },
+ },
+ },
+ },
+ {
+ "vrfs": {
+ "MGMT": {
+ "peers": {
+ "10.255.0.21": {
+ "inMsgQueue": 0,
+ "outMsgQueue": 0,
+ "peerState": "Established",
+ },
+ },
+ },
+ },
+ },
+ {
+ "vrfs": {
+ "default": {
+ "peers": {
+ "10.255.0.1": {
+ "inMsgQueue": 0,
+ "outMsgQueue": 0,
+ "peerState": "Established",
+ },
+ "10.255.0.2": {
"description": "DC1-SPINE2_Ethernet1",
- "version": 4,
- "msgReceived": 3759,
- "msgSent": 3757,
"inMsgQueue": 0,
"outMsgQueue": 0,
- "asn": "65100",
- "prefixAccepted": 14,
- "prefixReceived": 14,
- "upDownTime": 1694266296.367261,
- "underMaintenance": False,
"peerState": "Established",
},
},
- }
- }
- }
+ },
+ },
+ },
+ {
+ "vrfs": {
+ "default": {
+ "peers": {
+ "10.255.0.11": {
+ "inMsgQueue": 0,
+ "outMsgQueue": 0,
+ "peerState": "Established",
+ },
+ "10.255.0.12": {
+ "description": "DC1-SPINE2_Ethernet1",
+ "inMsgQueue": 0,
+ "outMsgQueue": 0,
+ "peerState": "Established",
+ },
+ },
+ },
+ },
+ },
+ {
+ "vrfs": {
+ "default": {
+ "peers": {
+ "10.255.0.21": {
+ "inMsgQueue": 0,
+ "outMsgQueue": 0,
+ "peerState": "Established",
+ },
+ "10.255.0.22": {
+ "inMsgQueue": 0,
+ "outMsgQueue": 0,
+ "peerState": "Established",
+ },
+ },
+ },
+ },
+ },
],
- "inputs": {"address_families": [{"afi": "ipv4", "safi": "unicast", "vrf": "default", "num_peers": 2}]},
+ "inputs": {
+ "address_families": [
+ {"afi": "ipv4", "safi": "unicast", "vrf": "default", "num_peers": 2},
+ {"afi": "ipv4", "safi": "sr-te", "vrf": "MGMT", "num_peers": 1},
+ {"afi": "evpn", "num_peers": 2},
+ {"afi": "link-state", "num_peers": 2},
+ {"afi": "path-selection", "num_peers": 2},
+ ]
+ },
"expected": {"result": "success"},
},
{
@@ -81,59 +136,189 @@ DATA: list[dict[str, Any]] = [
{
"vrfs": {
"default": {
- "vrf": "default",
- "routerId": "10.1.0.3",
- "asn": "65120",
"peers": {
"10.1.255.0": {
- "description": "DC1-SPINE1_Ethernet1",
- "version": 4,
- "msgReceived": 0,
- "msgSent": 0,
"inMsgQueue": 0,
"outMsgQueue": 0,
- "asn": "65100",
- "prefixAccepted": 0,
- "prefixReceived": 0,
- "upDownTime": 1694266295.098931,
- "underMaintenance": False,
"peerState": "Established",
},
"10.1.255.2": {
+ "inMsgQueue": 0,
+ "outMsgQueue": 0,
+ "peerState": "Established",
+ },
+ },
+ },
+ },
+ },
+ {
+ "vrfs": {
+ "MGMT": {
+ "peers": {
+ "10.255.0.21": {
+ "inMsgQueue": 0,
+ "outMsgQueue": 0,
+ "peerState": "Established",
+ },
+ },
+ },
+ },
+ },
+ {
+ "vrfs": {
+ "default": {
+ "peers": {
+ "10.255.0.1": {
+ "inMsgQueue": 0,
+ "outMsgQueue": 0,
+ "peerState": "Established",
+ },
+ "10.255.0.2": {
"description": "DC1-SPINE2_Ethernet1",
- "version": 4,
- "msgReceived": 3759,
- "msgSent": 3757,
"inMsgQueue": 0,
"outMsgQueue": 0,
- "asn": "65100",
- "prefixAccepted": 14,
- "prefixReceived": 14,
- "upDownTime": 1694266296.367261,
- "underMaintenance": False,
"peerState": "Established",
},
},
- }
- }
- }
+ },
+ },
+ },
+ {
+ "vrfs": {
+ "default": {
+ "peers": {
+ "10.255.0.11": {
+ "inMsgQueue": 0,
+ "outMsgQueue": 0,
+ "peerState": "Established",
+ },
+ "10.255.0.12": {
+ "description": "DC1-SPINE2_Ethernet1",
+ "inMsgQueue": 0,
+ "outMsgQueue": 0,
+ "peerState": "Established",
+ },
+ },
+ },
+ },
+ },
+ {
+ "vrfs": {
+ "default": {
+ "peers": {
+ "10.255.0.21": {
+ "inMsgQueue": 0,
+ "outMsgQueue": 0,
+ "peerState": "Established",
+ },
+ },
+ },
+ },
+ },
],
- "inputs": {"address_families": [{"afi": "ipv4", "safi": "unicast", "vrf": "default", "num_peers": 3}]},
- "expected": {"result": "failure", "messages": ["Failures: [{'afi': 'ipv4', 'safi': 'unicast', 'vrfs': {'default': 'Expected: 3, Actual: 2'}}]"]},
+ "inputs": {
+ "address_families": [
+ {"afi": "ipv4", "safi": "unicast", "vrf": "default", "num_peers": 3},
+ {"afi": "ipv4", "safi": "sr-te", "vrf": "MGMT", "num_peers": 2},
+ {"afi": "evpn", "num_peers": 1},
+ {"afi": "link-state", "num_peers": 3},
+ {"afi": "path-selection", "num_peers": 3},
+ ]
+ },
+ "expected": {
+ "result": "failure",
+ "messages": [
+ "Failures: [{'afi': 'ipv4', 'safi': 'unicast', 'vrfs': {'default': 'Expected: 3, Actual: 2'}}, "
+ "{'afi': 'ipv4', 'safi': 'sr-te', 'vrfs': {'MGMT': 'Expected: 2, Actual: 1'}}, "
+ "{'afi': 'evpn', 'vrfs': {'default': 'Expected: 1, Actual: 2'}}, "
+ "{'afi': 'link-state', 'vrfs': {'default': 'Expected: 3, Actual: 2'}}, "
+ "{'afi': 'path-selection', 'vrfs': {'default': 'Expected: 3, Actual: 1'}}]"
+ ],
+ },
},
{
"name": "failure-no-peers",
"test": VerifyBGPPeerCount,
- "eos_data": [{"vrfs": {"default": {"vrf": "default", "routerId": "10.1.0.3", "asn": "65120", "peers": {}}}}],
- "inputs": {"address_families": [{"afi": "ipv6", "safi": "unicast", "vrf": "default", "num_peers": 3}]},
- "expected": {"result": "failure", "messages": ["Failures: [{'afi': 'ipv6', 'safi': 'unicast', 'vrfs': {'default': 'Expected: 3, Actual: 0'}}]"]},
+ "eos_data": [
+ {
+ "vrfs": {
+ "default": {
+ "peers": {},
+ }
+ }
+ },
+ {
+ "vrfs": {
+ "MGMT": {
+ "peers": {},
+ }
+ }
+ },
+ {
+ "vrfs": {
+ "default": {
+ "peers": {},
+ }
+ }
+ },
+ {
+ "vrfs": {
+ "default": {
+ "peers": {},
+ }
+ }
+ },
+ {
+ "vrfs": {
+ "default": {
+ "peers": {},
+ }
+ }
+ },
+ ],
+ "inputs": {
+ "address_families": [
+ {"afi": "ipv4", "safi": "unicast", "vrf": "default", "num_peers": 2},
+ {"afi": "ipv4", "safi": "sr-te", "vrf": "MGMT", "num_peers": 1},
+ {"afi": "evpn", "num_peers": 2},
+ {"afi": "link-state", "num_peers": 2},
+ {"afi": "path-selection", "num_peers": 2},
+ ]
+ },
+ "expected": {
+ "result": "failure",
+ "messages": [
+ "Failures: [{'afi': 'ipv4', 'safi': 'unicast', 'vrfs': {'default': 'Expected: 2, Actual: 0'}}, "
+ "{'afi': 'ipv4', 'safi': 'sr-te', 'vrfs': {'MGMT': 'Expected: 1, Actual: 0'}}, "
+ "{'afi': 'evpn', 'vrfs': {'default': 'Expected: 2, Actual: 0'}}, "
+ "{'afi': 'link-state', 'vrfs': {'default': 'Expected: 2, Actual: 0'}}, "
+ "{'afi': 'path-selection', 'vrfs': {'default': 'Expected: 2, Actual: 0'}}]"
+ ],
+ },
},
{
"name": "failure-not-configured",
"test": VerifyBGPPeerCount,
- "eos_data": [{"vrfs": {}}],
- "inputs": {"address_families": [{"afi": "ipv6", "safi": "multicast", "vrf": "DEV", "num_peers": 3}]},
- "expected": {"result": "failure", "messages": ["Failures: [{'afi': 'ipv6', 'safi': 'multicast', 'vrfs': {'DEV': 'Not Configured'}}]"]},
+ "eos_data": [{"vrfs": {}}, {"vrfs": {}}, {"vrfs": {}}, {"vrfs": {}}, {"vrfs": {}}],
+ "inputs": {
+ "address_families": [
+ {"afi": "ipv6", "safi": "multicast", "vrf": "DEV", "num_peers": 3},
+ {"afi": "ipv4", "safi": "sr-te", "vrf": "MGMT", "num_peers": 1},
+ {"afi": "evpn", "num_peers": 2},
+ {"afi": "link-state", "num_peers": 2},
+ {"afi": "path-selection", "num_peers": 2},
+ ]
+ },
+ "expected": {
+ "result": "failure",
+ "messages": [
+ "Failures: [{'afi': 'ipv6', 'safi': 'multicast', 'vrfs': {'DEV': 'Not Configured'}}, "
+ "{'afi': 'ipv4', 'safi': 'sr-te', 'vrfs': {'MGMT': 'Not Configured'}}, "
+ "{'afi': 'evpn', 'vrfs': {'default': 'Not Configured'}}, "
+ "{'afi': 'link-state', 'vrfs': {'default': 'Not Configured'}}, "
+ "{'afi': 'path-selection', 'vrfs': {'default': 'Not Configured'}}]"
+ ],
+ },
},
{
"name": "success-vrf-all",
@@ -142,226 +327,132 @@ DATA: list[dict[str, Any]] = [
{
"vrfs": {
"default": {
- "vrf": "default",
- "routerId": "10.1.0.3",
- "asn": "65120",
"peers": {
"10.1.255.0": {
- "description": "DC1-SPINE1_Ethernet1",
- "version": 4,
- "msgReceived": 0,
- "msgSent": 0,
"inMsgQueue": 0,
"outMsgQueue": 0,
- "asn": "65100",
- "prefixAccepted": 0,
- "prefixReceived": 0,
- "upDownTime": 1694266295.098931,
- "underMaintenance": False,
"peerState": "Established",
},
},
},
"PROD": {
- "vrf": "PROD",
- "routerId": "10.1.0.3",
- "asn": "65120",
"peers": {
"10.1.254.1": {
- "description": "DC1-LEAF1B",
- "version": 4,
- "msgReceived": 3777,
- "msgSent": 3764,
"inMsgQueue": 0,
"outMsgQueue": 0,
- "asn": "65120",
- "prefixAccepted": 2,
- "prefixReceived": 2,
- "upDownTime": 1694266296.659891,
- "underMaintenance": False,
"peerState": "Established",
},
"192.168.1.11": {
- "description": "K8S-CLUSTER1",
- "version": 4,
- "msgReceived": 6417,
- "msgSent": 7546,
"inMsgQueue": 0,
"outMsgQueue": 0,
- "asn": "65000",
- "prefixAccepted": 1,
- "prefixReceived": 1,
- "upDownTime": 1694266329.978035,
- "underMaintenance": False,
"peerState": "Established",
},
},
},
- }
- }
- ],
- "inputs": {"address_families": [{"afi": "ipv4", "safi": "unicast", "vrf": "all", "num_peers": 3}]},
- "expected": {"result": "success"},
- },
- {
- "name": "failure-vrf-all",
- "test": VerifyBGPPeerCount,
- "eos_data": [
+ },
+ },
{
"vrfs": {
"default": {
- "vrf": "default",
- "routerId": "10.1.0.3",
- "asn": "65120",
"peers": {
- "10.1.255.0": {
- "description": "DC1-SPINE1_Ethernet1",
- "version": 4,
- "msgReceived": 0,
- "msgSent": 0,
+ "10.1.255.10": {
"inMsgQueue": 0,
"outMsgQueue": 0,
- "asn": "65100",
- "prefixAccepted": 0,
- "prefixReceived": 0,
- "upDownTime": 1694266295.098931,
- "underMaintenance": False,
"peerState": "Established",
},
},
},
"PROD": {
- "vrf": "PROD",
- "routerId": "10.1.0.3",
- "asn": "65120",
"peers": {
- "10.1.254.1": {
- "description": "DC1-LEAF1B",
- "version": 4,
- "msgReceived": 3777,
- "msgSent": 3764,
- "inMsgQueue": 0,
- "outMsgQueue": 0,
- "asn": "65120",
- "prefixAccepted": 2,
- "prefixReceived": 2,
- "upDownTime": 1694266296.659891,
- "underMaintenance": False,
- "peerState": "Established",
- },
- "192.168.1.11": {
- "description": "K8S-CLUSTER1",
- "version": 4,
- "msgReceived": 6417,
- "msgSent": 7546,
+ "10.1.254.11": {
"inMsgQueue": 0,
"outMsgQueue": 0,
- "asn": "65000",
- "prefixAccepted": 1,
- "prefixReceived": 1,
- "upDownTime": 1694266329.978035,
- "underMaintenance": False,
"peerState": "Established",
},
},
},
- }
- }
+ },
+ },
],
- "inputs": {"address_families": [{"afi": "ipv4", "safi": "unicast", "vrf": "all", "num_peers": 5}]},
- "expected": {"result": "failure", "messages": ["Failures: [{'afi': 'ipv4', 'safi': 'unicast', 'vrfs': {'all': 'Expected: 5, Actual: 3'}}]"]},
+ "inputs": {
+ "address_families": [
+ {"afi": "ipv4", "safi": "unicast", "vrf": "all", "num_peers": 3},
+ {"afi": "ipv4", "safi": "sr-te", "vrf": "all", "num_peers": 2},
+ ]
+ },
+ "expected": {"result": "success"},
},
{
- "name": "success-multiple-afi",
+ "name": "failure-vrf-all",
"test": VerifyBGPPeerCount,
"eos_data": [
{
"vrfs": {
+ "default": {
+ "peers": {
+ "10.1.255.0": {
+ "inMsgQueue": 0,
+ "outMsgQueue": 0,
+ "peerState": "Established",
+ },
+ },
+ },
"PROD": {
- "vrf": "PROD",
- "routerId": "10.1.0.3",
- "asn": "65120",
"peers": {
"10.1.254.1": {
- "description": "DC1-LEAF1B",
- "version": 4,
- "msgReceived": 3777,
- "msgSent": 3764,
"inMsgQueue": 0,
"outMsgQueue": 0,
- "asn": "65120",
- "prefixAccepted": 2,
- "prefixReceived": 2,
- "upDownTime": 1694266296.659891,
- "underMaintenance": False,
"peerState": "Established",
},
"192.168.1.11": {
- "description": "K8S-CLUSTER1",
- "version": 4,
- "msgReceived": 6417,
- "msgSent": 7546,
"inMsgQueue": 0,
"outMsgQueue": 0,
- "asn": "65000",
- "prefixAccepted": 1,
- "prefixReceived": 1,
- "upDownTime": 1694266329.978035,
- "underMaintenance": False,
"peerState": "Established",
},
},
- }
- }
+ },
+ },
},
{
"vrfs": {
"default": {
- "vrf": "default",
- "routerId": "10.1.0.3",
- "asn": "65120",
"peers": {
- "10.1.0.1": {
- "description": "DC1-SPINE1",
- "version": 4,
- "msgReceived": 3894,
- "msgSent": 3897,
+ "10.1.255.10": {
"inMsgQueue": 0,
"outMsgQueue": 0,
- "asn": "65100",
- "prefixAccepted": 0,
- "prefixReceived": 0,
- "upDownTime": 1694266296.584472,
- "underMaintenance": False,
"peerState": "Established",
},
- "10.1.0.2": {
- "description": "DC1-SPINE2",
- "version": 4,
- "msgReceived": 3893,
- "msgSent": 3902,
+ },
+ },
+ "PROD": {
+ "peers": {
+ "10.1.254.1": {
+ "inMsgQueue": 0,
+ "outMsgQueue": 0,
+ "peerState": "Established",
+ },
+ "192.168.1.12": {
"inMsgQueue": 0,
"outMsgQueue": 0,
- "asn": "65100",
- "prefixAccepted": 0,
- "prefixReceived": 0,
- "upDownTime": 1694266297.433896,
- "underMaintenance": False,
"peerState": "Established",
},
},
- }
- }
+ },
+ },
},
],
"inputs": {
"address_families": [
- {"afi": "ipv4", "safi": "unicast", "vrf": "PROD", "num_peers": 2},
- {"afi": "evpn", "num_peers": 2},
+ {"afi": "ipv4", "safi": "unicast", "vrf": "all", "num_peers": 5},
+ {"afi": "ipv4", "safi": "sr-te", "vrf": "all", "num_peers": 2},
]
},
"expected": {
- "result": "success",
+ "result": "failure",
+ "messages": [
+ "Failures: [{'afi': 'ipv4', 'safi': 'unicast', 'vrfs': {'all': 'Expected: 5, Actual: 3'}}, "
+ "{'afi': 'ipv4', 'safi': 'sr-te', 'vrfs': {'all': 'Expected: 2, Actual: 3'}}]"
+ ],
},
},
{
@@ -371,96 +462,114 @@ DATA: list[dict[str, Any]] = [
{
"vrfs": {
"PROD": {
- "vrf": "PROD",
- "routerId": "10.1.0.3",
- "asn": "65120",
"peers": {
"10.1.254.1": {
- "description": "DC1-LEAF1B",
- "version": 4,
- "msgReceived": 3777,
- "msgSent": 3764,
"inMsgQueue": 0,
"outMsgQueue": 0,
- "asn": "65120",
- "prefixAccepted": 2,
- "prefixReceived": 2,
- "upDownTime": 1694266296.659891,
- "underMaintenance": False,
"peerState": "Established",
},
"192.168.1.11": {
- "description": "K8S-CLUSTER1",
- "version": 4,
- "msgReceived": 6417,
- "msgSent": 7546,
"inMsgQueue": 0,
"outMsgQueue": 0,
- "asn": "65000",
- "prefixAccepted": 1,
- "prefixReceived": 1,
- "upDownTime": 1694266329.978035,
- "underMaintenance": False,
"peerState": "Established",
},
},
- }
- }
+ },
+ },
},
{"vrfs": {}},
{
"vrfs": {
+ "MGMT": {
+ "peers": {
+ "10.1.254.11": {
+ "inMsgQueue": 0,
+ "outMsgQueue": 0,
+ "peerState": "Established",
+ },
+ "192.168.1.21": {
+ "inMsgQueue": 0,
+ "outMsgQueue": 0,
+ "peerState": "Established",
+ },
+ },
+ },
+ },
+ },
+ {
+ "vrfs": {
"default": {
- "vrf": "default",
- "routerId": "10.1.0.3",
- "asn": "65120",
"peers": {
"10.1.0.1": {
- "description": "DC1-SPINE1",
- "version": 4,
- "msgReceived": 3894,
- "msgSent": 3897,
"inMsgQueue": 0,
"outMsgQueue": 0,
- "asn": "65100",
- "prefixAccepted": 0,
- "prefixReceived": 0,
- "upDownTime": 1694266296.584472,
- "underMaintenance": False,
"peerState": "Established",
},
"10.1.0.2": {
- "description": "DC1-SPINE2",
- "version": 4,
- "msgReceived": 3893,
- "msgSent": 3902,
"inMsgQueue": 0,
"outMsgQueue": 0,
- "asn": "65100",
- "prefixAccepted": 0,
- "prefixReceived": 0,
- "upDownTime": 1694266297.433896,
- "underMaintenance": False,
"peerState": "Established",
},
},
- }
- }
+ },
+ },
+ },
+ {
+ "vrfs": {
+ "default": {
+ "peers": {
+ "10.1.0.11": {
+ "inMsgQueue": 0,
+ "outMsgQueue": 0,
+ "peerState": "Established",
+ },
+ "10.1.0.21": {
+ "inMsgQueue": 0,
+ "outMsgQueue": 0,
+ "peerState": "Established",
+ },
+ },
+ },
+ },
+ },
+ {
+ "vrfs": {
+ "default": {
+ "peers": {
+ "10.1.0.2": {
+ "inMsgQueue": 0,
+ "outMsgQueue": 0,
+ "peerState": "Established",
+ },
+ "10.1.0.22": {
+ "inMsgQueue": 0,
+ "outMsgQueue": 0,
+ "peerState": "Established",
+ },
+ },
+ },
+ },
},
],
"inputs": {
"address_families": [
{"afi": "ipv4", "safi": "unicast", "vrf": "PROD", "num_peers": 3},
- {"afi": "evpn", "num_peers": 3},
{"afi": "ipv6", "safi": "unicast", "vrf": "default", "num_peers": 3},
- ]
+ {"afi": "ipv4", "safi": "sr-te", "vrf": "MGMT", "num_peers": 3},
+ {"afi": "evpn", "num_peers": 3},
+ {"afi": "link-state", "num_peers": 4},
+ {"afi": "path-selection", "num_peers": 1},
+ ],
},
"expected": {
"result": "failure",
"messages": [
"Failures: [{'afi': 'ipv4', 'safi': 'unicast', 'vrfs': {'PROD': 'Expected: 3, Actual: 2'}}, "
"{'afi': 'ipv6', 'safi': 'unicast', 'vrfs': {'default': 'Not Configured'}}, "
- "{'afi': 'evpn', 'vrfs': {'default': 'Expected: 3, Actual: 2'}}"
+ "{'afi': 'ipv4', 'safi': 'sr-te', 'vrfs': {'MGMT': 'Expected: 3, Actual: 2'}}, "
+ "{'afi': 'evpn', 'vrfs': {'default': 'Expected: 3, Actual: 2'}}, "
+ "{'afi': 'link-state', 'vrfs': {'default': 'Expected: 4, Actual: 2'}}, "
+ "{'afi': 'path-selection', 'vrfs': {'default': 'Expected: 1, Actual: 2'}}]",
],
},
},
@@ -471,44 +580,84 @@ DATA: list[dict[str, Any]] = [
{
"vrfs": {
"default": {
- "vrf": "default",
- "routerId": "10.1.0.3",
- "asn": "65120",
"peers": {
"10.1.255.0": {
- "description": "DC1-SPINE1_Ethernet1",
- "version": 4,
- "msgReceived": 0,
- "msgSent": 0,
"inMsgQueue": 0,
"outMsgQueue": 0,
- "asn": "65100",
- "prefixAccepted": 0,
- "prefixReceived": 0,
- "upDownTime": 1694266295.098931,
- "underMaintenance": False,
"peerState": "Established",
},
"10.1.255.2": {
- "description": "DC1-SPINE2_Ethernet1",
- "version": 4,
- "msgReceived": 3759,
- "msgSent": 3757,
"inMsgQueue": 0,
"outMsgQueue": 0,
- "asn": "65100",
- "prefixAccepted": 14,
- "prefixReceived": 14,
- "upDownTime": 1694266296.367261,
- "underMaintenance": False,
"peerState": "Established",
},
},
}
}
- }
+ },
+ {
+ "vrfs": {
+ "MGMT": {
+ "peers": {
+ "10.1.255.10": {
+ "inMsgQueue": 0,
+ "outMsgQueue": 0,
+ "peerState": "Established",
+ },
+ "10.1.255.12": {
+ "inMsgQueue": 0,
+ "outMsgQueue": 0,
+ "peerState": "Established",
+ },
+ },
+ }
+ }
+ },
+ {
+ "vrfs": {
+ "default": {
+ "peers": {
+ "10.1.255.20": {
+ "inMsgQueue": 0,
+ "outMsgQueue": 0,
+ "peerState": "Established",
+ },
+ "10.1.255.22": {
+ "inMsgQueue": 0,
+ "outMsgQueue": 0,
+ "peerState": "Established",
+ },
+ },
+ }
+ }
+ },
+ {
+ "vrfs": {
+ "default": {
+ "peers": {
+ "10.1.255.30": {
+ "inMsgQueue": 0,
+ "outMsgQueue": 0,
+ "peerState": "Established",
+ },
+ "10.1.255.32": {
+ "inMsgQueue": 0,
+ "outMsgQueue": 0,
+ "peerState": "Established",
+ },
+ },
+ }
+ }
+ },
],
- "inputs": {"address_families": [{"afi": "ipv4", "safi": "unicast", "vrf": "default"}]},
+ "inputs": {
+ "address_families": [
+ {"afi": "ipv4", "safi": "unicast", "vrf": "default"},
+ {"afi": "ipv4", "safi": "sr-te", "vrf": "MGMT"},
+ {"afi": "path-selection"},
+ {"afi": "link-state"},
+ ]
+ },
"expected": {"result": "success"},
},
{
@@ -518,48 +667,91 @@ DATA: list[dict[str, Any]] = [
{
"vrfs": {
"default": {
- "vrf": "default",
- "routerId": "10.1.0.3",
- "asn": "65120",
"peers": {
"10.1.255.0": {
- "description": "DC1-SPINE1_Ethernet1",
- "version": 4,
- "msgReceived": 0,
- "msgSent": 0,
"inMsgQueue": 0,
"outMsgQueue": 0,
- "asn": "65100",
- "prefixAccepted": 0,
- "prefixReceived": 0,
- "upDownTime": 1694266295.098931,
- "underMaintenance": False,
"peerState": "Idle",
},
"10.1.255.2": {
- "description": "DC1-SPINE2_Ethernet1",
- "version": 4,
- "msgReceived": 3759,
- "msgSent": 3757,
"inMsgQueue": 0,
"outMsgQueue": 0,
- "asn": "65100",
- "prefixAccepted": 14,
- "prefixReceived": 14,
- "upDownTime": 1694266296.367261,
- "underMaintenance": False,
"peerState": "Established",
},
},
}
}
- }
+ },
+ {
+ "vrfs": {
+ "MGMT": {
+ "peers": {
+ "10.1.255.10": {
+ "inMsgQueue": 0,
+ "outMsgQueue": 0,
+ "peerState": "Established",
+ },
+ "10.1.255.12": {
+ "inMsgQueue": 0,
+ "outMsgQueue": 0,
+ "peerState": "Idle",
+ },
+ },
+ }
+ }
+ },
+ {
+ "vrfs": {
+ "default": {
+ "peers": {
+ "10.1.255.20": {
+ "inMsgQueue": 0,
+ "outMsgQueue": 0,
+ "peerState": "Idle",
+ },
+ "10.1.255.22": {
+ "inMsgQueue": 0,
+ "outMsgQueue": 0,
+ "peerState": "Established",
+ },
+ },
+ }
+ }
+ },
+ {
+ "vrfs": {
+ "default": {
+ "peers": {
+ "10.1.255.30": {
+ "inMsgQueue": 0,
+ "outMsgQueue": 0,
+ "peerState": "Established",
+ },
+ "10.1.255.32": {
+ "inMsgQueue": 0,
+ "outMsgQueue": 0,
+ "peerState": "Idle",
+ },
+ },
+ }
+ }
+ },
],
- "inputs": {"address_families": [{"afi": "ipv4", "safi": "unicast", "vrf": "default"}]},
+ "inputs": {
+ "address_families": [
+ {"afi": "ipv4", "safi": "unicast", "vrf": "default"},
+ {"afi": "ipv4", "safi": "sr-te", "vrf": "MGMT"},
+ {"afi": "path-selection"},
+ {"afi": "link-state"},
+ ]
+ },
"expected": {
"result": "failure",
"messages": [
- "Failures: [{'afi': 'ipv4', 'safi': 'unicast', 'vrfs': {'default': {'10.1.255.0': {'peerState': 'Idle', 'inMsgQueue': 0, 'outMsgQueue': 0}}}}]"
+ "Failures: [{'afi': 'ipv4', 'safi': 'unicast', 'vrfs': {'default': {'10.1.255.0': {'peerState': 'Idle', 'inMsgQueue': 0, 'outMsgQueue': 0}}}}, "
+ "{'afi': 'ipv4', 'safi': 'sr-te', 'vrfs': {'MGMT': {'10.1.255.12': {'peerState': 'Idle', 'inMsgQueue': 0, 'outMsgQueue': 0}}}}, "
+ "{'afi': 'path-selection', 'vrfs': {'default': {'10.1.255.20': {'peerState': 'Idle', 'inMsgQueue': 0, 'outMsgQueue': 0}}}}, "
+ "{'afi': 'link-state', 'vrfs': {'default': {'10.1.255.32': {'peerState': 'Idle', 'inMsgQueue': 0, 'outMsgQueue': 0}}}}]"
],
},
},
@@ -570,79 +762,74 @@ DATA: list[dict[str, Any]] = [
{
"vrfs": {
"default": {
- "vrf": "default",
- "routerId": "10.1.0.3",
- "asn": "65120",
"peers": {
"10.1.255.0": {
- "description": "DC1-SPINE1_Ethernet1",
- "version": 4,
- "msgReceived": 0,
- "msgSent": 0,
"inMsgQueue": 0,
"outMsgQueue": 0,
- "asn": "65100",
- "prefixAccepted": 0,
- "prefixReceived": 0,
- "upDownTime": 1694266295.098931,
- "underMaintenance": False,
"peerState": "Established",
},
"10.1.255.2": {
- "description": "DC1-SPINE2_Ethernet1",
- "version": 4,
- "msgReceived": 3759,
- "msgSent": 3757,
"inMsgQueue": 0,
"outMsgQueue": 0,
- "asn": "65100",
- "prefixAccepted": 14,
- "prefixReceived": 14,
- "upDownTime": 1694266296.367261,
- "underMaintenance": False,
"peerState": "Established",
},
},
},
"PROD": {
- "vrf": "PROD",
- "routerId": "10.1.0.3",
- "asn": "65120",
"peers": {
"10.1.254.1": {
- "description": "DC1-LEAF1B",
- "version": 4,
- "msgReceived": 3777,
- "msgSent": 3764,
"inMsgQueue": 0,
"outMsgQueue": 0,
- "asn": "65120",
- "prefixAccepted": 2,
- "prefixReceived": 2,
- "upDownTime": 1694266296.659891,
- "underMaintenance": False,
"peerState": "Established",
},
"192.168.1.11": {
- "description": "K8S-CLUSTER1",
- "version": 4,
- "msgReceived": 6417,
- "msgSent": 7546,
"inMsgQueue": 0,
"outMsgQueue": 0,
- "asn": "65000",
- "prefixAccepted": 1,
- "prefixReceived": 1,
- "upDownTime": 1694266329.978035,
- "underMaintenance": False,
"peerState": "Established",
},
},
},
}
- }
+ },
+ {
+ "vrfs": {
+ "default": {
+ "peers": {
+ "10.1.255.10": {
+ "inMsgQueue": 0,
+ "outMsgQueue": 0,
+ "peerState": "Established",
+ },
+ "10.1.255.12": {
+ "inMsgQueue": 0,
+ "outMsgQueue": 0,
+ "peerState": "Established",
+ },
+ },
+ },
+ "PROD": {
+ "peers": {
+ "10.1.254.11": {
+ "inMsgQueue": 0,
+ "outMsgQueue": 0,
+ "peerState": "Established",
+ },
+ "192.168.1.111": {
+ "inMsgQueue": 0,
+ "outMsgQueue": 0,
+ "peerState": "Established",
+ },
+ },
+ },
+ }
+ },
],
- "inputs": {"address_families": [{"afi": "ipv4", "safi": "unicast", "vrf": "all"}]},
+ "inputs": {
+ "address_families": [
+ {"afi": "ipv4", "safi": "unicast", "vrf": "all"},
+ {"afi": "ipv4", "safi": "sr-te", "vrf": "all"},
+ ]
+ },
"expected": {
"result": "success",
},
@@ -654,138 +841,183 @@ DATA: list[dict[str, Any]] = [
{
"vrfs": {
"default": {
- "vrf": "default",
- "routerId": "10.1.0.3",
- "asn": "65120",
"peers": {
"10.1.255.0": {
- "description": "DC1-SPINE1_Ethernet1",
- "version": 4,
- "msgReceived": 0,
- "msgSent": 0,
"inMsgQueue": 0,
"outMsgQueue": 0,
- "asn": "65100",
- "prefixAccepted": 0,
- "prefixReceived": 0,
- "upDownTime": 1694266295.098931,
- "underMaintenance": False,
"peerState": "Idle",
},
"10.1.255.2": {
- "description": "DC1-SPINE2_Ethernet1",
- "version": 4,
- "msgReceived": 3759,
- "msgSent": 3757,
"inMsgQueue": 0,
"outMsgQueue": 0,
- "asn": "65100",
- "prefixAccepted": 14,
- "prefixReceived": 14,
- "upDownTime": 1694266296.367261,
- "underMaintenance": False,
"peerState": "Established",
},
},
},
"PROD": {
- "vrf": "PROD",
- "routerId": "10.1.0.3",
- "asn": "65120",
"peers": {
"10.1.254.1": {
- "description": "DC1-LEAF1B",
- "version": 4,
- "msgReceived": 3777,
- "msgSent": 3764,
"inMsgQueue": 0,
"outMsgQueue": 0,
- "asn": "65120",
- "prefixAccepted": 2,
- "prefixReceived": 2,
- "upDownTime": 1694266296.659891,
- "underMaintenance": False,
"peerState": "Established",
},
"192.168.1.11": {
- "description": "K8S-CLUSTER1",
- "version": 4,
- "msgReceived": 6417,
- "msgSent": 7546,
"inMsgQueue": 100,
"outMsgQueue": 200,
- "asn": "65000",
- "prefixAccepted": 1,
- "prefixReceived": 1,
- "upDownTime": 1694266329.978035,
- "underMaintenance": False,
"peerState": "Established",
},
},
},
}
- }
+ },
+ {
+ "vrfs": {
+ "default": {
+ "peers": {
+ "10.1.255.10": {
+ "inMsgQueue": 0,
+ "outMsgQueue": 0,
+ "peerState": "Idle",
+ },
+ "10.1.255.12": {
+ "inMsgQueue": 0,
+ "outMsgQueue": 0,
+ "peerState": "Established",
+ },
+ },
+ },
+ "PROD": {
+ "peers": {
+ "10.1.254.11": {
+ "inMsgQueue": 0,
+ "outMsgQueue": 0,
+ "peerState": "Established",
+ },
+ "192.168.1.111": {
+ "inMsgQueue": 100,
+ "outMsgQueue": 200,
+ "peerState": "Established",
+ },
+ },
+ },
+ }
+ },
],
- "inputs": {"address_families": [{"afi": "ipv4", "safi": "unicast", "vrf": "all"}]},
+ "inputs": {
+ "address_families": [
+ {"afi": "ipv4", "safi": "unicast", "vrf": "all"},
+ {"afi": "ipv4", "safi": "sr-te", "vrf": "all"},
+ ]
+ },
"expected": {
"result": "failure",
"messages": [
"Failures: [{'afi': 'ipv4', 'safi': 'unicast', 'vrfs': {'default': {'10.1.255.0': {'peerState': 'Idle', 'inMsgQueue': 0, 'outMsgQueue': 0}}, "
- "'PROD': {'192.168.1.11': {'peerState': 'Established', 'inMsgQueue': 100, 'outMsgQueue': 200}}}}]"
+ "'PROD': {'192.168.1.11': {'peerState': 'Established', 'inMsgQueue': 100, 'outMsgQueue': 200}}}}, "
+ "{'afi': 'ipv4', 'safi': 'sr-te', 'vrfs': {'default': {'10.1.255.10': {'peerState': 'Idle', 'inMsgQueue': 0, 'outMsgQueue': 0}}, "
+ "'PROD': {'192.168.1.111': {'peerState': 'Established', 'inMsgQueue': 100, 'outMsgQueue': 200}}}}]"
],
},
},
{
"name": "failure-not-configured",
"test": VerifyBGPPeersHealth,
- "eos_data": [{"vrfs": {}}],
- "inputs": {"address_families": [{"afi": "ipv4", "safi": "unicast", "vrf": "DEV"}]},
- "expected": {"result": "failure", "messages": ["Failures: [{'afi': 'ipv4', 'safi': 'unicast', 'vrfs': {'DEV': 'Not Configured'}}]"]},
+ "eos_data": [{"vrfs": {}}, {"vrfs": {}}, {"vrfs": {}}, {"vrfs": {}}],
+ "inputs": {
+ "address_families": [
+ {"afi": "ipv4", "safi": "unicast", "vrf": "DEV"},
+ {"afi": "ipv4", "safi": "sr-te", "vrf": "MGMT"},
+ {"afi": "link-state"},
+ {"afi": "path-selection"},
+ ]
+ },
+ "expected": {
+ "result": "failure",
+ "messages": [
+ "Failures: [{'afi': 'ipv4', 'safi': 'unicast', 'vrfs': {'DEV': 'Not Configured'}}, "
+ "{'afi': 'ipv4', 'safi': 'sr-te', 'vrfs': {'MGMT': 'Not Configured'}}, "
+ "{'afi': 'link-state', 'vrfs': {'default': 'Not Configured'}}, "
+ "{'afi': 'path-selection', 'vrfs': {'default': 'Not Configured'}}]"
+ ],
+ },
},
{
"name": "failure-no-peers",
"test": VerifyBGPPeersHealth,
- "eos_data": [{"vrfs": {"default": {"vrf": "default", "routerId": "10.1.0.3", "asn": "65120", "peers": {}}}}],
- "inputs": {"address_families": [{"afi": "ipv4", "safi": "multicast"}]},
- "expected": {"result": "failure", "messages": ["Failures: [{'afi': 'ipv4', 'safi': 'multicast', 'vrfs': {'default': 'No Peers'}}]"]},
- },
- {
- "name": "success-multiple-afi",
- "test": VerifyBGPPeersHealth,
"eos_data": [
{
"vrfs": {
- "PROD": {
- "vrf": "PROD",
+ "default": {
+ "vrf": "default",
"routerId": "10.1.0.3",
"asn": "65120",
+ "peers": {},
+ }
+ }
+ },
+ {
+ "vrfs": {
+ "MGMT": {
+ "vrf": "MGMT",
+ "routerId": "10.1.0.3",
+ "asn": "65120",
+ "peers": {},
+ }
+ }
+ },
+ {
+ "vrfs": {
+ "default": {
+ "vrf": "default",
+ "routerId": "10.1.0.3",
+ "asn": "65120",
+ "peers": {},
+ }
+ }
+ },
+ {
+ "vrfs": {
+ "default": {
+ "vrf": "default",
+ "routerId": "10.1.0.3",
+ "asn": "65120",
+ "peers": {},
+ }
+ }
+ },
+ ],
+ "inputs": {
+ "address_families": [
+ {"afi": "ipv4", "safi": "multicast"},
+ {"afi": "ipv4", "safi": "sr-te", "vrf": "MGMT"},
+ {"afi": "link-state"},
+ {"afi": "path-selection"},
+ ]
+ },
+ "expected": {
+ "result": "failure",
+ "messages": [
+ "Failures: [{'afi': 'ipv4', 'safi': 'multicast', 'vrfs': {'default': 'No Peers'}}, {'afi': 'ipv4', 'safi': 'sr-te', 'vrfs': {'MGMT': 'No Peers'}}, "
+ "{'afi': 'link-state', 'vrfs': {'default': 'No Peers'}}, {'afi': 'path-selection', 'vrfs': {'default': 'No Peers'}}]"
+ ],
+ },
+ },
+ {
+ "name": "success",
+ "test": VerifyBGPSpecificPeers,
+ "eos_data": [
+ {
+ "vrfs": {
+ "default": {
"peers": {
- "10.1.254.1": {
- "description": "DC1-LEAF1B",
- "version": 4,
- "msgReceived": 3777,
- "msgSent": 3764,
+ "10.1.255.0": {
"inMsgQueue": 0,
"outMsgQueue": 0,
- "asn": "65120",
- "prefixAccepted": 2,
- "prefixReceived": 2,
- "upDownTime": 1694266296.659891,
- "underMaintenance": False,
"peerState": "Established",
},
- "192.168.1.11": {
- "description": "K8S-CLUSTER1",
- "version": 4,
- "msgReceived": 6417,
- "msgSent": 7546,
+ "10.1.255.2": {
"inMsgQueue": 0,
"outMsgQueue": 0,
- "asn": "65000",
- "prefixAccepted": 1,
- "prefixReceived": 1,
- "upDownTime": 1694266329.978035,
- "underMaintenance": False,
"peerState": "Established",
},
},
@@ -794,132 +1026,53 @@ DATA: list[dict[str, Any]] = [
},
{
"vrfs": {
- "default": {
- "vrf": "default",
- "routerId": "10.1.0.3",
- "asn": "65120",
+ "MGMT": {
"peers": {
- "10.1.0.1": {
- "description": "DC1-SPINE1",
- "version": 4,
- "msgReceived": 3894,
- "msgSent": 3897,
+ "10.1.255.10": {
"inMsgQueue": 0,
"outMsgQueue": 0,
- "asn": "65100",
- "prefixAccepted": 0,
- "prefixReceived": 0,
- "upDownTime": 1694266296.584472,
- "underMaintenance": False,
"peerState": "Established",
},
- "10.1.0.2": {
- "description": "DC1-SPINE2",
- "version": 4,
- "msgReceived": 3893,
- "msgSent": 3902,
+ "10.1.255.12": {
"inMsgQueue": 0,
"outMsgQueue": 0,
- "asn": "65100",
- "prefixAccepted": 0,
- "prefixReceived": 0,
- "upDownTime": 1694266297.433896,
- "underMaintenance": False,
"peerState": "Established",
},
},
}
}
},
- ],
- "inputs": {
- "address_families": [
- {"afi": "ipv4", "safi": "unicast", "vrf": "PROD"},
- {"afi": "evpn"},
- ]
- },
- "expected": {
- "result": "success",
- },
- },
- {
- "name": "failure-multiple-afi",
- "test": VerifyBGPPeersHealth,
- "eos_data": [
{
"vrfs": {
- "PROD": {
- "vrf": "PROD",
- "routerId": "10.1.0.3",
- "asn": "65120",
+ "default": {
"peers": {
- "10.1.254.1": {
- "description": "DC1-LEAF1B",
- "version": 4,
- "msgReceived": 3777,
- "msgSent": 3764,
+ "10.1.255.20": {
"inMsgQueue": 0,
"outMsgQueue": 0,
- "asn": "65120",
- "prefixAccepted": 2,
- "prefixReceived": 2,
- "upDownTime": 1694266296.659891,
- "underMaintenance": False,
"peerState": "Established",
},
- "192.168.1.11": {
- "description": "K8S-CLUSTER1",
- "version": 4,
- "msgReceived": 6417,
- "msgSent": 7546,
- "inMsgQueue": 10,
+ "10.1.255.22": {
+ "inMsgQueue": 0,
"outMsgQueue": 0,
- "asn": "65000",
- "prefixAccepted": 1,
- "prefixReceived": 1,
- "upDownTime": 1694266329.978035,
- "underMaintenance": False,
"peerState": "Established",
},
},
}
}
},
- {"vrfs": {}},
{
"vrfs": {
"default": {
- "vrf": "default",
- "routerId": "10.1.0.3",
- "asn": "65120",
"peers": {
- "10.1.0.1": {
- "description": "DC1-SPINE1",
- "version": 4,
- "msgReceived": 3894,
- "msgSent": 3897,
+ "10.1.255.30": {
"inMsgQueue": 0,
"outMsgQueue": 0,
- "asn": "65100",
- "prefixAccepted": 0,
- "prefixReceived": 0,
- "upDownTime": 1694266296.584472,
- "underMaintenance": False,
"peerState": "Established",
},
- "10.1.0.2": {
- "description": "DC1-SPINE2",
- "version": 4,
- "msgReceived": 3893,
- "msgSent": 3902,
+ "10.1.255.32": {
"inMsgQueue": 0,
"outMsgQueue": 0,
- "asn": "65100",
- "prefixAccepted": 0,
- "prefixReceived": 0,
- "upDownTime": 1694266297.433896,
- "underMaintenance": False,
- "peerState": "Idle",
+ "peerState": "Established",
},
},
}
@@ -928,174 +1081,76 @@ DATA: list[dict[str, Any]] = [
],
"inputs": {
"address_families": [
- {"afi": "ipv4", "safi": "unicast", "vrf": "PROD"},
- {"afi": "evpn"},
- {"afi": "ipv6", "safi": "unicast", "vrf": "default"},
+ {
+ "afi": "ipv4",
+ "safi": "unicast",
+ "vrf": "default",
+ "peers": ["10.1.255.0", "10.1.255.2"],
+ },
+ {
+ "afi": "ipv4",
+ "safi": "sr-te",
+ "vrf": "MGMT",
+ "peers": ["10.1.255.10", "10.1.255.12"],
+ },
+ {"afi": "path-selection", "peers": ["10.1.255.20", "10.1.255.22"]},
+ {"afi": "link-state", "peers": ["10.1.255.30", "10.1.255.32"]},
]
},
- "expected": {
- "result": "failure",
- "messages": [
- "Failures: [{'afi': 'ipv4', 'safi': 'unicast', 'vrfs': "
- "{'PROD': {'192.168.1.11': {'peerState': 'Established', 'inMsgQueue': 10, 'outMsgQueue': 0}}}}, "
- "{'afi': 'ipv6', 'safi': 'unicast', 'vrfs': {'default': 'Not Configured'}}, "
- "{'afi': 'evpn', 'vrfs': {'default': {'10.1.0.2': {'peerState': 'Idle', 'inMsgQueue': 0, 'outMsgQueue': 0}}}"
- ],
- },
+ "expected": {"result": "success"},
},
{
- "name": "success",
+ "name": "failure-issues",
"test": VerifyBGPSpecificPeers,
"eos_data": [
{
"vrfs": {
"default": {
- "vrf": "default",
- "routerId": "10.1.0.3",
- "asn": "65120",
"peers": {
"10.1.255.0": {
- "description": "DC1-SPINE1_Ethernet1",
- "version": 4,
- "msgReceived": 0,
- "msgSent": 0,
"inMsgQueue": 0,
"outMsgQueue": 0,
- "asn": "65100",
- "prefixAccepted": 0,
- "prefixReceived": 0,
- "upDownTime": 1694266295.098931,
- "underMaintenance": False,
- "peerState": "Established",
+ "peerState": "Idle",
},
"10.1.255.2": {
- "description": "DC1-SPINE2_Ethernet1",
- "version": 4,
- "msgReceived": 3759,
- "msgSent": 3757,
"inMsgQueue": 0,
"outMsgQueue": 0,
- "asn": "65100",
- "prefixAccepted": 14,
- "prefixReceived": 14,
- "upDownTime": 1694266296.367261,
- "underMaintenance": False,
"peerState": "Established",
},
},
}
}
- }
- ],
- "inputs": {"address_families": [{"afi": "ipv4", "safi": "unicast", "vrf": "default", "peers": ["10.1.255.0", "10.1.255.2"]}]},
- "expected": {"result": "success"},
- },
- {
- "name": "failure-issues",
- "test": VerifyBGPSpecificPeers,
- "eos_data": [
+ },
{
"vrfs": {
- "default": {
- "vrf": "default",
- "routerId": "10.1.0.3",
- "asn": "65120",
+ "MGMT": {
"peers": {
- "10.1.255.0": {
- "description": "DC1-SPINE1_Ethernet1",
- "version": 4,
- "msgReceived": 0,
- "msgSent": 0,
+ "10.1.255.10": {
"inMsgQueue": 0,
"outMsgQueue": 0,
- "asn": "65100",
- "prefixAccepted": 0,
- "prefixReceived": 0,
- "upDownTime": 1694266295.098931,
- "underMaintenance": False,
- "peerState": "Idle",
+ "peerState": "Established",
},
- "10.1.255.2": {
- "description": "DC1-SPINE2_Ethernet1",
- "version": 4,
- "msgReceived": 3759,
- "msgSent": 3757,
+ "10.1.255.12": {
"inMsgQueue": 0,
"outMsgQueue": 0,
- "asn": "65100",
- "prefixAccepted": 14,
- "prefixReceived": 14,
- "upDownTime": 1694266296.367261,
- "underMaintenance": False,
- "peerState": "Established",
+ "peerState": "Idle",
},
},
}
}
- }
- ],
- "inputs": {"address_families": [{"afi": "ipv4", "safi": "unicast", "vrf": "default", "peers": ["10.1.255.0", "10.1.255.2"]}]},
- "expected": {
- "result": "failure",
- "messages": [
- "Failures: [{'afi': 'ipv4', 'safi': 'unicast', 'vrfs': {'default': {'10.1.255.0': {'peerState': 'Idle', 'inMsgQueue': 0, 'outMsgQueue': 0}}}}]"
- ],
- },
- },
- {
- "name": "failure-not-configured",
- "test": VerifyBGPSpecificPeers,
- "eos_data": [{"vrfs": {}}],
- "inputs": {"address_families": [{"afi": "ipv4", "safi": "unicast", "vrf": "DEV", "peers": ["10.1.255.0"]}]},
- "expected": {"result": "failure", "messages": ["Failures: [{'afi': 'ipv4', 'safi': 'unicast', 'vrfs': {'DEV': 'Not Configured'}}]"]},
- },
- {
- "name": "failure-no-peers",
- "test": VerifyBGPSpecificPeers,
- "eos_data": [{"vrfs": {"default": {"vrf": "default", "routerId": "10.1.0.3", "asn": "65120", "peers": {}}}}],
- "inputs": {"address_families": [{"afi": "ipv4", "safi": "multicast", "peers": ["10.1.255.0"]}]},
- "expected": {
- "result": "failure",
- "messages": ["Failures: [{'afi': 'ipv4', 'safi': 'multicast', 'vrfs': {'default': {'10.1.255.0': {'peerNotFound': True}}}}]"],
- },
- },
- {
- "name": "success-multiple-afi",
- "test": VerifyBGPSpecificPeers,
- "eos_data": [
+ },
{
"vrfs": {
- "PROD": {
- "vrf": "PROD",
- "routerId": "10.1.0.3",
- "asn": "65120",
+ "default": {
"peers": {
- "10.1.254.1": {
- "description": "DC1-LEAF1B",
- "version": 4,
- "msgReceived": 3777,
- "msgSent": 3764,
+ "10.1.255.20": {
"inMsgQueue": 0,
"outMsgQueue": 0,
- "asn": "65120",
- "prefixAccepted": 2,
- "prefixReceived": 2,
- "upDownTime": 1694266296.659891,
- "underMaintenance": False,
- "peerState": "Established",
+ "peerState": "Idle",
},
- "192.168.1.11": {
- "description": "K8S-CLUSTER1",
- "version": 4,
- "msgReceived": 6417,
- "msgSent": 7546,
+ "10.1.255.22": {
"inMsgQueue": 0,
"outMsgQueue": 0,
- "asn": "65000",
- "prefixAccepted": 1,
- "prefixReceived": 1,
- "upDownTime": 1694266329.978035,
- "underMaintenance": False,
"peerState": "Established",
},
},
@@ -1105,37 +1160,16 @@ DATA: list[dict[str, Any]] = [
{
"vrfs": {
"default": {
- "vrf": "default",
- "routerId": "10.1.0.3",
- "asn": "65120",
"peers": {
- "10.1.0.1": {
- "description": "DC1-SPINE1",
- "version": 4,
- "msgReceived": 3894,
- "msgSent": 3897,
+ "10.1.255.30": {
"inMsgQueue": 0,
"outMsgQueue": 0,
- "asn": "65100",
- "prefixAccepted": 0,
- "prefixReceived": 0,
- "upDownTime": 1694266296.584472,
- "underMaintenance": False,
"peerState": "Established",
},
- "10.1.0.2": {
- "description": "DC1-SPINE2",
- "version": 4,
- "msgReceived": 3893,
- "msgSent": 3902,
+ "10.1.255.32": {
"inMsgQueue": 0,
"outMsgQueue": 0,
- "asn": "65100",
- "prefixAccepted": 0,
- "prefixReceived": 0,
- "upDownTime": 1694266297.433896,
- "underMaintenance": False,
- "peerState": "Established",
+ "peerState": "Idle",
},
},
}
@@ -1144,110 +1178,128 @@ DATA: list[dict[str, Any]] = [
],
"inputs": {
"address_families": [
- {"afi": "ipv4", "safi": "unicast", "vrf": "PROD", "peers": ["10.1.254.1", "192.168.1.11"]},
- {"afi": "evpn", "peers": ["10.1.0.1", "10.1.0.2"]},
+ {
+ "afi": "ipv4",
+ "safi": "unicast",
+ "vrf": "default",
+ "peers": ["10.1.255.0", "10.1.255.2"],
+ },
+ {
+ "afi": "ipv4",
+ "safi": "sr-te",
+ "vrf": "MGMT",
+ "peers": ["10.1.255.10", "10.1.255.12"],
+ },
+ {"afi": "path-selection", "peers": ["10.1.255.20", "10.1.255.22"]},
+ {"afi": "link-state", "peers": ["10.1.255.30", "10.1.255.32"]},
]
},
- "expected": {"result": "success"},
+ "expected": {
+ "result": "failure",
+ "messages": [
+ "Failures: [{'afi': 'ipv4', 'safi': 'unicast', 'vrfs': {'default': {'10.1.255.0': {'peerState': 'Idle', 'inMsgQueue': 0, 'outMsgQueue': 0}}}}, "
+ "{'afi': 'ipv4', 'safi': 'sr-te', 'vrfs': {'MGMT': {'10.1.255.12': {'peerState': 'Idle', 'inMsgQueue': 0, 'outMsgQueue': 0}}}}, "
+ "{'afi': 'path-selection', 'vrfs': {'default': {'10.1.255.20': {'peerState': 'Idle', 'inMsgQueue': 0, 'outMsgQueue': 0}}}}, "
+ "{'afi': 'link-state', 'vrfs': {'default': {'10.1.255.32': {'peerState': 'Idle', 'inMsgQueue': 0, 'outMsgQueue': 0}}}}]"
+ ],
+ },
},
{
- "name": "failure-multiple-afi",
+ "name": "failure-not-configured",
+ "test": VerifyBGPSpecificPeers,
+ "eos_data": [{"vrfs": {}}, {"vrfs": {}}, {"vrfs": {}}, {"vrfs": {}}],
+ "inputs": {
+ "address_families": [
+ {
+ "afi": "ipv4",
+ "safi": "unicast",
+ "vrf": "DEV",
+ "peers": ["10.1.255.0"],
+ },
+ {
+ "afi": "ipv4",
+ "safi": "sr-te",
+ "vrf": "MGMT",
+ "peers": ["10.1.255.10"],
+ },
+ {"afi": "link-state", "peers": ["10.1.255.20"]},
+ {"afi": "path-selection", "peers": ["10.1.255.30"]},
+ ]
+ },
+ "expected": {
+ "result": "failure",
+ "messages": [
+ "Failures: [{'afi': 'ipv4', 'safi': 'unicast', 'vrfs': {'DEV': 'Not Configured'}}, "
+ "{'afi': 'ipv4', 'safi': 'sr-te', 'vrfs': {'MGMT': 'Not Configured'}}, {'afi': 'link-state', 'vrfs': {'default': 'Not Configured'}}, "
+ "{'afi': 'path-selection', 'vrfs': {'default': 'Not Configured'}}]"
+ ],
+ },
+ },
+ {
+ "name": "failure-no-peers",
"test": VerifyBGPSpecificPeers,
"eos_data": [
{
"vrfs": {
- "PROD": {
- "vrf": "PROD",
+ "default": {
+ "vrf": "default",
"routerId": "10.1.0.3",
"asn": "65120",
- "peers": {
- "10.1.254.1": {
- "description": "DC1-LEAF1B",
- "version": 4,
- "msgReceived": 3777,
- "msgSent": 3764,
- "inMsgQueue": 0,
- "outMsgQueue": 0,
- "asn": "65120",
- "prefixAccepted": 2,
- "prefixReceived": 2,
- "upDownTime": 1694266296.659891,
- "underMaintenance": False,
- "peerState": "Established",
- },
- "192.168.1.11": {
- "description": "K8S-CLUSTER1",
- "version": 4,
- "msgReceived": 6417,
- "msgSent": 7546,
- "inMsgQueue": 10,
- "outMsgQueue": 0,
- "asn": "65000",
- "prefixAccepted": 1,
- "prefixReceived": 1,
- "upDownTime": 1694266329.978035,
- "underMaintenance": False,
- "peerState": "Established",
- },
- },
+ "peers": {},
+ }
+ }
+ },
+ {
+ "vrfs": {
+ "MGMT": {
+ "vrf": "MGMT",
+ "routerId": "10.1.0.3",
+ "asn": "65120",
+ "peers": {},
}
}
},
- {"vrfs": {}},
{
"vrfs": {
"default": {
"vrf": "default",
"routerId": "10.1.0.3",
"asn": "65120",
- "peers": {
- "10.1.0.1": {
- "description": "DC1-SPINE1",
- "version": 4,
- "msgReceived": 3894,
- "msgSent": 3897,
- "inMsgQueue": 0,
- "outMsgQueue": 0,
- "asn": "65100",
- "prefixAccepted": 0,
- "prefixReceived": 0,
- "upDownTime": 1694266296.584472,
- "underMaintenance": False,
- "peerState": "Established",
- },
- "10.1.0.2": {
- "description": "DC1-SPINE2",
- "version": 4,
- "msgReceived": 3893,
- "msgSent": 3902,
- "inMsgQueue": 0,
- "outMsgQueue": 0,
- "asn": "65100",
- "prefixAccepted": 0,
- "prefixReceived": 0,
- "upDownTime": 1694266297.433896,
- "underMaintenance": False,
- "peerState": "Idle",
- },
- },
+ "peers": {},
+ }
+ }
+ },
+ {
+ "vrfs": {
+ "default": {
+ "vrf": "default",
+ "routerId": "10.1.0.3",
+ "asn": "65120",
+ "peers": {},
}
}
},
],
"inputs": {
"address_families": [
- {"afi": "ipv4", "safi": "unicast", "vrf": "PROD", "peers": ["10.1.254.1", "192.168.1.11"]},
- {"afi": "evpn", "peers": ["10.1.0.1", "10.1.0.2"]},
- {"afi": "ipv6", "safi": "unicast", "vrf": "default", "peers": ["10.1.0.1", "10.1.0.2"]},
+ {"afi": "ipv4", "safi": "multicast", "peers": ["10.1.255.0"]},
+ {
+ "afi": "ipv4",
+ "safi": "sr-te",
+ "vrf": "MGMT",
+ "peers": ["10.1.255.10"],
+ },
+ {"afi": "link-state", "peers": ["10.1.255.20"]},
+ {"afi": "path-selection", "peers": ["10.1.255.30"]},
]
},
"expected": {
"result": "failure",
"messages": [
- "Failures: [{'afi': 'ipv4', 'safi': 'unicast', 'vrfs': "
- "{'PROD': {'192.168.1.11': {'peerState': 'Established', 'inMsgQueue': 10, 'outMsgQueue': 0}}}}, "
- "{'afi': 'ipv6', 'safi': 'unicast', 'vrfs': {'default': 'Not Configured'}}, "
- "{'afi': 'evpn', 'vrfs': {'default': {'10.1.0.2': {'peerState': 'Idle', 'inMsgQueue': 0, 'outMsgQueue': 0}}}"
+ "Failures: [{'afi': 'ipv4', 'safi': 'multicast', 'vrfs': {'default': {'10.1.255.0': {'peerNotFound': True}}}}, "
+ "{'afi': 'ipv4', 'safi': 'sr-te', 'vrfs': {'MGMT': {'10.1.255.10': {'peerNotFound': True}}}}, "
+ "{'afi': 'link-state', 'vrfs': {'default': {'10.1.255.20': {'peerNotFound': True}}}}, "
+ "{'afi': 'path-selection', 'vrfs': {'default': {'10.1.255.30': {'peerNotFound': True}}}}]"
],
},
},
@@ -1390,10 +1442,46 @@ DATA: list[dict[str, Any]] = [
"name": "failure-no-routes",
"test": VerifyBGPExchangedRoutes,
"eos_data": [
- {"vrfs": {"default": {"vrf": "default", "routerId": "192.0.255.1", "asn": "65001", "bgpRouteEntries": {}}}},
- {"vrfs": {"default": {"vrf": "default", "routerId": "192.0.255.1", "asn": "65001", "bgpRouteEntries": {}}}},
- {"vrfs": {"default": {"vrf": "default", "routerId": "192.0.255.1", "asn": "65001", "bgpRouteEntries": {}}}},
- {"vrfs": {"default": {"vrf": "default", "routerId": "192.0.255.1", "asn": "65001", "bgpRouteEntries": {}}}},
+ {
+ "vrfs": {
+ "default": {
+ "vrf": "default",
+ "routerId": "192.0.255.1",
+ "asn": "65001",
+ "bgpRouteEntries": {},
+ }
+ }
+ },
+ {
+ "vrfs": {
+ "default": {
+ "vrf": "default",
+ "routerId": "192.0.255.1",
+ "asn": "65001",
+ "bgpRouteEntries": {},
+ }
+ }
+ },
+ {
+ "vrfs": {
+ "default": {
+ "vrf": "default",
+ "routerId": "192.0.255.1",
+ "asn": "65001",
+ "bgpRouteEntries": {},
+ }
+ }
+ },
+ {
+ "vrfs": {
+ "default": {
+ "vrf": "default",
+ "routerId": "192.0.255.1",
+ "asn": "65001",
+ "bgpRouteEntries": {},
+ }
+ }
+ },
],
"inputs": {
"bgp_peers": [
@@ -1801,8 +1889,16 @@ DATA: list[dict[str, Any]] = [
"peerAddress": "172.30.11.1",
"neighborCapabilities": {
"multiprotocolCaps": {
- "ipv4Unicast": {"advertised": True, "received": True, "enabled": True},
- "ipv4MplsLabels": {"advertised": True, "received": True, "enabled": True},
+ "ipv4Unicast": {
+ "advertised": True,
+ "received": True,
+ "enabled": True,
+ },
+ "ipv4MplsLabels": {
+ "advertised": True,
+ "received": True,
+ "enabled": True,
+ },
}
},
}
@@ -1814,8 +1910,16 @@ DATA: list[dict[str, Any]] = [
"peerAddress": "172.30.11.10",
"neighborCapabilities": {
"multiprotocolCaps": {
- "ipv4Unicast": {"advertised": True, "received": True, "enabled": True},
- "ipv4MplsVpn": {"advertised": True, "received": True, "enabled": True},
+ "ipv4Unicast": {
+ "advertised": True,
+ "received": True,
+ "enabled": True,
+ },
+ "ipv4MplsVpn": {
+ "advertised": True,
+ "received": True,
+ "enabled": True,
+ },
}
},
}
@@ -1852,8 +1956,16 @@ DATA: list[dict[str, Any]] = [
"peerAddress": "172.30.11.1",
"neighborCapabilities": {
"multiprotocolCaps": {
- "ipv4Unicast": {"advertised": True, "received": True, "enabled": True},
- "ipv4MplsVpn": {"advertised": True, "received": True, "enabled": True},
+ "ipv4Unicast": {
+ "advertised": True,
+ "received": True,
+ "enabled": True,
+ },
+ "ipv4MplsVpn": {
+ "advertised": True,
+ "received": True,
+ "enabled": True,
+ },
}
},
}
@@ -1889,7 +2001,13 @@ DATA: list[dict[str, Any]] = [
{
"peerAddress": "172.30.11.1",
"neighborCapabilities": {
- "multiprotocolCaps": {"ipv4Unicast": {"advertised": True, "received": True, "enabled": True}},
+ "multiprotocolCaps": {
+ "ipv4Unicast": {
+ "advertised": True,
+ "received": True,
+ "enabled": True,
+ }
+ },
},
}
]
@@ -1899,7 +2017,13 @@ DATA: list[dict[str, Any]] = [
{
"peerAddress": "172.30.11.10",
"neighborCapabilities": {
- "multiprotocolCaps": {"ipv4Unicast": {"advertised": True, "received": True, "enabled": True}},
+ "multiprotocolCaps": {
+ "ipv4Unicast": {
+ "advertised": True,
+ "received": True,
+ "enabled": True,
+ }
+ },
},
}
]
@@ -1940,7 +2064,13 @@ DATA: list[dict[str, Any]] = [
{
"peerAddress": "172.30.11.1",
"neighborCapabilities": {
- "multiprotocolCaps": {"ipv4Unicast": {"advertised": True, "received": True, "enabled": True}},
+ "multiprotocolCaps": {
+ "ipv4Unicast": {
+ "advertised": True,
+ "received": True,
+ "enabled": True,
+ }
+ },
},
}
]
@@ -1948,7 +2078,15 @@ DATA: list[dict[str, Any]] = [
}
}
],
- "inputs": {"bgp_peers": [{"peer_address": "172.30.11.1", "vrf": "default", "capabilities": ["ipv4 Unicast", "L2VpnEVPN"]}]},
+ "inputs": {
+ "bgp_peers": [
+ {
+ "peer_address": "172.30.11.1",
+ "vrf": "default",
+ "capabilities": ["ipv4 Unicast", "L2VpnEVPN"],
+ }
+ ]
+ },
"expected": {
"result": "failure",
"messages": [
@@ -1968,8 +2106,16 @@ DATA: list[dict[str, Any]] = [
"peerAddress": "172.30.11.1",
"neighborCapabilities": {
"multiprotocolCaps": {
- "ipv4Unicast": {"advertised": False, "received": False, "enabled": False},
- "ipv4MplsVpn": {"advertised": False, "received": True, "enabled": False},
+ "ipv4Unicast": {
+ "advertised": False,
+ "received": False,
+ "enabled": False,
+ },
+ "ipv4MplsVpn": {
+ "advertised": False,
+ "received": True,
+ "enabled": False,
+ },
},
},
}
@@ -1981,8 +2127,16 @@ DATA: list[dict[str, Any]] = [
"peerAddress": "172.30.11.10",
"neighborCapabilities": {
"multiprotocolCaps": {
- "l2VpnEvpn": {"advertised": True, "received": False, "enabled": False},
- "ipv4MplsVpn": {"advertised": False, "received": False, "enabled": True},
+ "l2VpnEvpn": {
+ "advertised": True,
+ "received": False,
+ "enabled": False,
+ },
+ "ipv4MplsVpn": {
+ "advertised": False,
+ "received": False,
+ "enabled": True,
+ },
},
},
},
@@ -1990,8 +2144,16 @@ DATA: list[dict[str, Any]] = [
"peerAddress": "172.30.11.11",
"neighborCapabilities": {
"multiprotocolCaps": {
- "ipv4Unicast": {"advertised": False, "received": False, "enabled": False},
- "ipv4MplsVpn": {"advertised": False, "received": False, "enabled": False},
+ "ipv4Unicast": {
+ "advertised": False,
+ "received": False,
+ "enabled": False,
+ },
+ "ipv4MplsVpn": {
+ "advertised": False,
+ "received": False,
+ "enabled": False,
+ },
},
},
},
@@ -2002,9 +2164,21 @@ DATA: list[dict[str, Any]] = [
],
"inputs": {
"bgp_peers": [
- {"peer_address": "172.30.11.1", "vrf": "default", "capabilities": ["ipv4 unicast", "ipv4 mpls vpn", "L2 vpn EVPN"]},
- {"peer_address": "172.30.11.10", "vrf": "MGMT", "capabilities": ["ipv4unicast", "ipv4 mplsvpn", "L2vpnEVPN"]},
- {"peer_address": "172.30.11.11", "vrf": "MGMT", "capabilities": ["Ipv4 Unicast", "ipv4 MPLSVPN", "L2 vpnEVPN"]},
+ {
+ "peer_address": "172.30.11.1",
+ "vrf": "default",
+ "capabilities": ["ipv4 unicast", "ipv4 mpls vpn", "L2 vpn EVPN"],
+ },
+ {
+ "peer_address": "172.30.11.10",
+ "vrf": "MGMT",
+ "capabilities": ["ipv4unicast", "ipv4 mplsvpn", "L2vpnEVPN"],
+ },
+ {
+ "peer_address": "172.30.11.11",
+ "vrf": "MGMT",
+ "capabilities": ["Ipv4 Unicast", "ipv4 MPLSVPN", "L2 vpnEVPN"],
+ },
]
},
"expected": {
@@ -2031,7 +2205,11 @@ DATA: list[dict[str, Any]] = [
{
"peerAddress": "172.30.11.1",
"neighborCapabilities": {
- "fourOctetAsnCap": {"advertised": True, "received": True, "enabled": True},
+ "fourOctetAsnCap": {
+ "advertised": True,
+ "received": True,
+ "enabled": True,
+ },
},
}
]
@@ -2041,7 +2219,11 @@ DATA: list[dict[str, Any]] = [
{
"peerAddress": "172.30.11.10",
"neighborCapabilities": {
- "fourOctetAsnCap": {"advertised": True, "received": True, "enabled": True},
+ "fourOctetAsnCap": {
+ "advertised": True,
+ "received": True,
+ "enabled": True,
+ },
},
}
]
@@ -2074,7 +2256,11 @@ DATA: list[dict[str, Any]] = [
{
"peerAddress": "172.30.11.1",
"neighborCapabilities": {
- "fourOctetAsnCap": {"advertised": True, "received": True, "enabled": True},
+ "fourOctetAsnCap": {
+ "advertised": True,
+ "received": True,
+ "enabled": True,
+ },
},
}
]
@@ -2085,7 +2271,11 @@ DATA: list[dict[str, Any]] = [
{
"peerAddress": "172.30.11.10",
"neighborCapabilities": {
- "fourOctetAsnCap": {"advertised": True, "received": True, "enabled": True},
+ "fourOctetAsnCap": {
+ "advertised": True,
+ "received": True,
+ "enabled": True,
+ },
},
}
]
@@ -2123,7 +2313,13 @@ DATA: list[dict[str, Any]] = [
{
"peerAddress": "172.30.11.1",
"neighborCapabilities": {
- "multiprotocolCaps": {"ipv4Unicast": {"advertised": True, "received": True, "enabled": True}},
+ "multiprotocolCaps": {
+ "ipv4Unicast": {
+ "advertised": True,
+ "received": True,
+ "enabled": True,
+ }
+ },
},
},
]
@@ -2157,7 +2353,13 @@ DATA: list[dict[str, Any]] = [
{
"peerAddress": "172.30.11.1",
"neighborCapabilities": {
- "multiprotocolCaps": {"ipv4Unicast": {"advertised": True, "received": True, "enabled": True}},
+ "multiprotocolCaps": {
+ "ipv4Unicast": {
+ "advertised": True,
+ "received": True,
+ "enabled": True,
+ }
+ },
},
}
]
@@ -2167,7 +2369,13 @@ DATA: list[dict[str, Any]] = [
{
"peerAddress": "172.30.11.10",
"neighborCapabilities": {
- "multiprotocolCaps": {"ipv4MplsLabels": {"advertised": True, "received": True, "enabled": True}},
+ "multiprotocolCaps": {
+ "ipv4MplsLabels": {
+ "advertised": True,
+ "received": True,
+ "enabled": True,
+ }
+ },
},
}
]
@@ -2175,7 +2383,12 @@ DATA: list[dict[str, Any]] = [
}
}
],
- "inputs": {"bgp_peers": [{"peer_address": "172.30.11.1", "vrf": "default"}, {"peer_address": "172.30.11.10", "vrf": "MGMT"}]},
+ "inputs": {
+ "bgp_peers": [
+ {"peer_address": "172.30.11.1", "vrf": "default"},
+ {"peer_address": "172.30.11.10", "vrf": "MGMT"},
+ ]
+ },
"expected": {
"result": "failure",
"messages": [
@@ -2195,7 +2408,11 @@ DATA: list[dict[str, Any]] = [
{
"peerAddress": "172.30.11.1",
"neighborCapabilities": {
- "fourOctetAsnCap": {"advertised": False, "received": False, "enabled": False},
+ "fourOctetAsnCap": {
+ "advertised": False,
+ "received": False,
+ "enabled": False,
+ },
},
}
]
@@ -2205,7 +2422,11 @@ DATA: list[dict[str, Any]] = [
{
"peerAddress": "172.30.11.10",
"neighborCapabilities": {
- "fourOctetAsnCap": {"advertised": True, "received": False, "enabled": True},
+ "fourOctetAsnCap": {
+ "advertised": True,
+ "received": False,
+ "enabled": True,
+ },
},
}
]
@@ -2213,7 +2434,12 @@ DATA: list[dict[str, Any]] = [
}
}
],
- "inputs": {"bgp_peers": [{"peer_address": "172.30.11.1", "vrf": "default"}, {"peer_address": "172.30.11.10", "vrf": "MGMT"}]},
+ "inputs": {
+ "bgp_peers": [
+ {"peer_address": "172.30.11.1", "vrf": "default"},
+ {"peer_address": "172.30.11.10", "vrf": "MGMT"},
+ ]
+ },
"expected": {
"result": "failure",
"messages": [
@@ -2234,7 +2460,11 @@ DATA: list[dict[str, Any]] = [
{
"peerAddress": "172.30.11.1",
"neighborCapabilities": {
- "routeRefreshCap": {"advertised": True, "received": True, "enabled": True},
+ "routeRefreshCap": {
+ "advertised": True,
+ "received": True,
+ "enabled": True,
+ },
},
}
]
@@ -2244,7 +2474,11 @@ DATA: list[dict[str, Any]] = [
{
"peerAddress": "172.30.11.11",
"neighborCapabilities": {
- "routeRefreshCap": {"advertised": True, "received": True, "enabled": True},
+ "routeRefreshCap": {
+ "advertised": True,
+ "received": True,
+ "enabled": True,
+ },
},
}
]
@@ -2296,7 +2530,13 @@ DATA: list[dict[str, Any]] = [
{
"peerAddress": "172.30.11.1",
"neighborCapabilities": {
- "multiprotocolCaps": {"ip4Unicast": {"advertised": True, "received": True, "enabled": True}},
+ "multiprotocolCaps": {
+ "ip4Unicast": {
+ "advertised": True,
+ "received": True,
+ "enabled": True,
+ }
+ },
},
}
]
@@ -2306,7 +2546,13 @@ DATA: list[dict[str, Any]] = [
{
"peerAddress": "172.30.11.12",
"neighborCapabilities": {
- "multiprotocolCaps": {"ip4Unicast": {"advertised": True, "received": True, "enabled": True}},
+ "multiprotocolCaps": {
+ "ip4Unicast": {
+ "advertised": True,
+ "received": True,
+ "enabled": True,
+ }
+ },
},
}
]
@@ -2345,7 +2591,13 @@ DATA: list[dict[str, Any]] = [
{
"peerAddress": "172.30.11.1",
"neighborCapabilities": {
- "multiprotocolCaps": {"ipv4Unicast": {"advertised": True, "received": True, "enabled": True}},
+ "multiprotocolCaps": {
+ "ipv4Unicast": {
+ "advertised": True,
+ "received": True,
+ "enabled": True,
+ }
+ },
},
}
]
@@ -2355,7 +2607,13 @@ DATA: list[dict[str, Any]] = [
{
"peerAddress": "172.30.11.11",
"neighborCapabilities": {
- "multiprotocolCaps": {"ipv4Unicast": {"advertised": True, "received": True, "enabled": True}},
+ "multiprotocolCaps": {
+ "ipv4Unicast": {
+ "advertised": True,
+ "received": True,
+ "enabled": True,
+ }
+ },
},
}
]
@@ -2363,7 +2621,12 @@ DATA: list[dict[str, Any]] = [
}
}
],
- "inputs": {"bgp_peers": [{"peer_address": "172.30.11.1", "vrf": "default"}, {"peer_address": "172.30.11.11", "vrf": "CS"}]},
+ "inputs": {
+ "bgp_peers": [
+ {"peer_address": "172.30.11.1", "vrf": "default"},
+ {"peer_address": "172.30.11.11", "vrf": "CS"},
+ ]
+ },
"expected": {
"result": "failure",
"messages": [
@@ -2383,7 +2646,11 @@ DATA: list[dict[str, Any]] = [
{
"peerAddress": "172.30.11.1",
"neighborCapabilities": {
- "routeRefreshCap": {"advertised": False, "received": False, "enabled": False},
+ "routeRefreshCap": {
+ "advertised": False,
+ "received": False,
+ "enabled": False,
+ },
},
}
]
@@ -2393,7 +2660,11 @@ DATA: list[dict[str, Any]] = [
{
"peerAddress": "172.30.11.11",
"neighborCapabilities": {
- "routeRefreshCap": {"advertised": True, "received": True, "enabled": True},
+ "routeRefreshCap": {
+ "advertised": True,
+ "received": True,
+ "enabled": True,
+ },
},
}
]
@@ -2401,7 +2672,12 @@ DATA: list[dict[str, Any]] = [
}
}
],
- "inputs": {"bgp_peers": [{"peer_address": "172.30.11.1", "vrf": "default"}, {"peer_address": "172.30.11.11", "vrf": "CS"}]},
+ "inputs": {
+ "bgp_peers": [
+ {"peer_address": "172.30.11.1", "vrf": "default"},
+ {"peer_address": "172.30.11.11", "vrf": "CS"},
+ ]
+ },
"expected": {
"result": "failure",
"messages": [
@@ -2592,10 +2868,22 @@ DATA: list[dict[str, Any]] = [
"peerAddress": "172.30.11.1",
"state": "Established",
},
- {"peerAddress": "172.30.11.10", "state": "Established", "md5AuthEnabled": False},
+ {
+ "peerAddress": "172.30.11.10",
+ "state": "Established",
+ "md5AuthEnabled": False,
+ },
+ ]
+ },
+ "MGMT": {
+ "peerList": [
+ {
+ "peerAddress": "172.30.11.11",
+ "state": "Established",
+ "md5AuthEnabled": False,
+ }
]
},
- "MGMT": {"peerList": [{"peerAddress": "172.30.11.11", "state": "Established", "md5AuthEnabled": False}]},
}
}
],
@@ -2684,7 +2972,12 @@ DATA: list[dict[str, Any]] = [
},
},
],
- "inputs": {"vxlan_endpoints": [{"address": "192.168.20.102", "vni": 10020}, {"address": "aac1.ab5d.b41e", "vni": 10010}]},
+ "inputs": {
+ "vxlan_endpoints": [
+ {"address": "192.168.20.102", "vni": 10020},
+ {"address": "aac1.ab5d.b41e", "vni": 10010},
+ ]
+ },
"expected": {"result": "success"},
},
{
@@ -3024,7 +3317,12 @@ DATA: list[dict[str, Any]] = [
},
},
],
- "inputs": {"vxlan_endpoints": [{"address": "192.168.20.102", "vni": 10020}, {"address": "aac1.ab5d.b41e", "vni": 10010}]},
+ "inputs": {
+ "vxlan_endpoints": [
+ {"address": "192.168.20.102", "vni": 10020},
+ {"address": "aac1.ab5d.b41e", "vni": 10010},
+ ]
+ },
"expected": {
"result": "failure",
"messages": [
@@ -3057,7 +3355,12 @@ DATA: list[dict[str, Any]] = [
},
},
],
- "inputs": {"vxlan_endpoints": [{"address": "aac1.ab4e.bec2", "vni": 10020}, {"address": "192.168.10.101", "vni": 10010}]},
+ "inputs": {
+ "vxlan_endpoints": [
+ {"address": "aac1.ab4e.bec2", "vni": 10020},
+ {"address": "192.168.10.101", "vni": 10010},
+ ]
+ },
"expected": {
"result": "failure",
"messages": [
@@ -3074,7 +3377,12 @@ DATA: list[dict[str, Any]] = [
{"vrf": "default", "routerId": "10.1.0.3", "asn": 65120, "evpnRoutes": {}},
{"vrf": "default", "routerId": "10.1.0.3", "asn": 65120, "evpnRoutes": {}},
],
- "inputs": {"vxlan_endpoints": [{"address": "aac1.ab4e.bec2", "vni": 10020}, {"address": "192.168.10.101", "vni": 10010}]},
+ "inputs": {
+ "vxlan_endpoints": [
+ {"address": "aac1.ab4e.bec2", "vni": 10020},
+ {"address": "192.168.10.101", "vni": 10010},
+ ]
+ },
"expected": {
"result": "failure",
"messages": ["The following VXLAN endpoint do not have any EVPN Type-2 route: [('aa:c1:ab:4e:be:c2', 10020), ('192.168.10.101', 10010)]"],
@@ -3090,7 +3398,11 @@ DATA: list[dict[str, Any]] = [
"peerList": [
{
"peerAddress": "172.30.11.1",
- "advertisedCommunities": {"standard": True, "extended": True, "large": True},
+ "advertisedCommunities": {
+ "standard": True,
+ "extended": True,
+ "large": True,
+ },
}
]
},
@@ -3098,7 +3410,11 @@ DATA: list[dict[str, Any]] = [
"peerList": [
{
"peerAddress": "172.30.11.10",
- "advertisedCommunities": {"standard": True, "extended": True, "large": True},
+ "advertisedCommunities": {
+ "standard": True,
+ "extended": True,
+ "large": True,
+ },
}
]
},
@@ -3128,7 +3444,11 @@ DATA: list[dict[str, Any]] = [
"peerList": [
{
"peerAddress": "172.30.11.1",
- "advertisedCommunities": {"standard": True, "extended": True, "large": True},
+ "advertisedCommunities": {
+ "standard": True,
+ "extended": True,
+ "large": True,
+ },
}
]
},
@@ -3161,7 +3481,11 @@ DATA: list[dict[str, Any]] = [
"peerList": [
{
"peerAddress": "172.30.11.1",
- "advertisedCommunities": {"standard": True, "extended": True, "large": True},
+ "advertisedCommunities": {
+ "standard": True,
+ "extended": True,
+ "large": True,
+ },
}
]
},
@@ -3169,7 +3493,11 @@ DATA: list[dict[str, Any]] = [
"peerList": [
{
"peerAddress": "172.30.11.1",
- "advertisedCommunities": {"standard": True, "extended": True, "large": True},
+ "advertisedCommunities": {
+ "standard": True,
+ "extended": True,
+ "large": True,
+ },
}
]
},
@@ -3206,7 +3534,11 @@ DATA: list[dict[str, Any]] = [
"peerList": [
{
"peerAddress": "172.30.11.1",
- "advertisedCommunities": {"standard": False, "extended": False, "large": False},
+ "advertisedCommunities": {
+ "standard": False,
+ "extended": False,
+ "large": False,
+ },
}
]
},
@@ -3214,7 +3546,11 @@ DATA: list[dict[str, Any]] = [
"peerList": [
{
"peerAddress": "172.30.11.10",
- "advertisedCommunities": {"standard": True, "extended": True, "large": False},
+ "advertisedCommunities": {
+ "standard": True,
+ "extended": True,
+ "large": False,
+ },
}
]
},
diff --git a/tests/units/anta_tests/routing/test_generic.py b/tests/units/anta_tests/routing/test_generic.py
index 90e70f8..36658f5 100644
--- a/tests/units/anta_tests/routing/test_generic.py
+++ b/tests/units/anta_tests/routing/test_generic.py
@@ -1,9 +1,8 @@
# 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.
-"""
-Tests for anta.tests.routing.generic.py
-"""
+"""Tests for anta.tests.routing.generic.py."""
+
from __future__ import annotations
from typing import Any
@@ -43,9 +42,9 @@ DATA: list[dict[str, Any]] = [
# Output truncated
"maskLen": {"8": 2},
"totalRoutes": 123,
- }
+ },
},
- }
+ },
],
"inputs": {"minimum": 42, "maximum": 666},
"expected": {"result": "success"},
@@ -60,9 +59,9 @@ DATA: list[dict[str, Any]] = [
# Output truncated
"maskLen": {"8": 2},
"totalRoutes": 1000,
- }
+ },
},
- }
+ },
],
"inputs": {"minimum": 42, "maximum": 666},
"expected": {"result": "failure", "messages": ["routing-table has 1000 routes and not between min (42) and maximum (666)"]},
@@ -99,10 +98,10 @@ DATA: list[dict[str, Any]] = [
"preference": 20,
"metric": 0,
"vias": [{"nexthopAddr": "10.1.255.4", "interface": "Ethernet1"}],
- }
+ },
},
- }
- }
+ },
+ },
},
{
"vrfs": {
@@ -122,10 +121,10 @@ DATA: list[dict[str, Any]] = [
"preference": 20,
"metric": 0,
"vias": [{"nexthopAddr": "10.1.255.6", "interface": "Ethernet2"}],
- }
+ },
},
- }
- }
+ },
+ },
},
],
"inputs": {"vrf": "default", "routes": ["10.1.0.1", "10.1.0.2"]},
@@ -143,8 +142,8 @@ DATA: list[dict[str, Any]] = [
"allRoutesProgrammedKernel": True,
"defaultRouteState": "notSet",
"routes": {},
- }
- }
+ },
+ },
},
{
"vrfs": {
@@ -164,10 +163,10 @@ DATA: list[dict[str, Any]] = [
"preference": 20,
"metric": 0,
"vias": [{"nexthopAddr": "10.1.255.6", "interface": "Ethernet2"}],
- }
+ },
},
- }
- }
+ },
+ },
},
],
"inputs": {"vrf": "default", "routes": ["10.1.0.1", "10.1.0.2"]},
@@ -195,10 +194,10 @@ DATA: list[dict[str, Any]] = [
"preference": 20,
"metric": 0,
"vias": [{"nexthopAddr": "10.1.255.4", "interface": "Ethernet1"}],
- }
+ },
},
- }
- }
+ },
+ },
},
{
"vrfs": {
@@ -218,10 +217,10 @@ DATA: list[dict[str, Any]] = [
"preference": 20,
"metric": 0,
"vias": [{"nexthopAddr": "10.1.255.6", "interface": "Ethernet2"}],
- }
+ },
},
- }
- }
+ },
+ },
},
],
"inputs": {"vrf": "default", "routes": ["10.1.0.1", "10.1.0.2"]},
diff --git a/tests/units/anta_tests/routing/test_ospf.py b/tests/units/anta_tests/routing/test_ospf.py
index fbabee9..81d8010 100644
--- a/tests/units/anta_tests/routing/test_ospf.py
+++ b/tests/units/anta_tests/routing/test_ospf.py
@@ -1,14 +1,13 @@
# 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.
-"""
-Tests for anta.tests.routing.ospf.py
-"""
+"""Tests for anta.tests.routing.ospf.py."""
+
from __future__ import annotations
from typing import Any
-from anta.tests.routing.ospf import VerifyOSPFNeighborCount, VerifyOSPFNeighborState
+from anta.tests.routing.ospf import VerifyOSPFMaxLSA, VerifyOSPFNeighborCount, VerifyOSPFNeighborState
from tests.lib.anta import test # noqa: F401; pylint: disable=W0611
DATA: list[dict[str, Any]] = [
@@ -40,9 +39,9 @@ DATA: list[dict[str, Any]] = [
"inactivity": 1683298014.844345,
"interfaceAddress": "10.3.0.1",
},
- ]
- }
- }
+ ],
+ },
+ },
},
"BLAH": {
"instList": {
@@ -56,13 +55,13 @@ DATA: list[dict[str, Any]] = [
"adjacencyState": "full",
"inactivity": 1683298014.844345,
"interfaceAddress": "10.3.0.1",
- }
- ]
- }
- }
+ },
+ ],
+ },
+ },
},
- }
- }
+ },
+ },
],
"inputs": None,
"expected": {"result": "success"},
@@ -95,9 +94,9 @@ DATA: list[dict[str, Any]] = [
"inactivity": 1683298014.844345,
"interfaceAddress": "10.3.0.1",
},
- ]
- }
- }
+ ],
+ },
+ },
},
"BLAH": {
"instList": {
@@ -111,20 +110,20 @@ DATA: list[dict[str, Any]] = [
"adjacencyState": "down",
"inactivity": 1683298014.844345,
"interfaceAddress": "10.3.0.1",
- }
- ]
- }
- }
+ },
+ ],
+ },
+ },
},
- }
- }
+ },
+ },
],
"inputs": None,
"expected": {
"result": "failure",
"messages": [
"Some neighbors are not correctly configured: [{'vrf': 'default', 'instance': '666', 'neighbor': '7.7.7.7', 'state': '2-way'},"
- " {'vrf': 'BLAH', 'instance': '777', 'neighbor': '8.8.8.8', 'state': 'down'}]."
+ " {'vrf': 'BLAH', 'instance': '777', 'neighbor': '8.8.8.8', 'state': 'down'}].",
],
},
},
@@ -134,7 +133,7 @@ DATA: list[dict[str, Any]] = [
"eos_data": [
{
"vrfs": {},
- }
+ },
],
"inputs": None,
"expected": {"result": "skipped", "messages": ["no OSPF neighbor found"]},
@@ -167,9 +166,9 @@ DATA: list[dict[str, Any]] = [
"inactivity": 1683298014.844345,
"interfaceAddress": "10.3.0.1",
},
- ]
- }
- }
+ ],
+ },
+ },
},
"BLAH": {
"instList": {
@@ -183,13 +182,13 @@ DATA: list[dict[str, Any]] = [
"adjacencyState": "full",
"inactivity": 1683298014.844345,
"interfaceAddress": "10.3.0.1",
- }
- ]
- }
- }
+ },
+ ],
+ },
+ },
},
- }
- }
+ },
+ },
],
"inputs": {"number": 3},
"expected": {"result": "success"},
@@ -213,12 +212,12 @@ DATA: list[dict[str, Any]] = [
"inactivity": 1683298014.844345,
"interfaceAddress": "10.3.0.1",
},
- ]
- }
- }
- }
- }
- }
+ ],
+ },
+ },
+ },
+ },
+ },
],
"inputs": {"number": 3},
"expected": {"result": "failure", "messages": ["device has 1 neighbors (expected 3)"]},
@@ -251,9 +250,9 @@ DATA: list[dict[str, Any]] = [
"inactivity": 1683298014.844345,
"interfaceAddress": "10.3.0.1",
},
- ]
- }
- }
+ ],
+ },
+ },
},
"BLAH": {
"instList": {
@@ -267,20 +266,20 @@ DATA: list[dict[str, Any]] = [
"adjacencyState": "down",
"inactivity": 1683298014.844345,
"interfaceAddress": "10.3.0.1",
- }
- ]
- }
- }
+ },
+ ],
+ },
+ },
},
- }
- }
+ },
+ },
],
"inputs": {"number": 3},
"expected": {
"result": "failure",
"messages": [
"Some neighbors are not correctly configured: [{'vrf': 'default', 'instance': '666', 'neighbor': '7.7.7.7', 'state': '2-way'},"
- " {'vrf': 'BLAH', 'instance': '777', 'neighbor': '8.8.8.8', 'state': 'down'}]."
+ " {'vrf': 'BLAH', 'instance': '777', 'neighbor': '8.8.8.8', 'state': 'down'}].",
],
},
},
@@ -290,9 +289,123 @@ DATA: list[dict[str, Any]] = [
"eos_data": [
{
"vrfs": {},
- }
+ },
],
"inputs": {"number": 3},
"expected": {"result": "skipped", "messages": ["no OSPF neighbor found"]},
},
+ {
+ "name": "success",
+ "test": VerifyOSPFMaxLSA,
+ "eos_data": [
+ {
+ "vrfs": {
+ "default": {
+ "instList": {
+ "1": {
+ "instanceId": 1,
+ "maxLsaInformation": {
+ "maxLsa": 12000,
+ "maxLsaThreshold": 75,
+ },
+ "routerId": "1.1.1.1",
+ "lsaInformation": {
+ "lsaArrivalInterval": 1000,
+ "lsaStartInterval": 1000,
+ "lsaHoldInterval": 5000,
+ "lsaMaxWaitInterval": 5000,
+ "numLsa": 9,
+ },
+ },
+ },
+ },
+ "TEST": {
+ "instList": {
+ "10": {
+ "instanceId": 10,
+ "maxLsaInformation": {
+ "maxLsa": 1000,
+ "maxLsaThreshold": 75,
+ },
+ "routerId": "20.20.20.20",
+ "lsaInformation": {
+ "lsaArrivalInterval": 1000,
+ "lsaStartInterval": 1000,
+ "lsaHoldInterval": 5000,
+ "lsaMaxWaitInterval": 5000,
+ "numLsa": 5,
+ },
+ },
+ },
+ },
+ },
+ },
+ ],
+ "inputs": None,
+ "expected": {"result": "success"},
+ },
+ {
+ "name": "failure",
+ "test": VerifyOSPFMaxLSA,
+ "eos_data": [
+ {
+ "vrfs": {
+ "default": {
+ "instList": {
+ "1": {
+ "instanceId": 1,
+ "maxLsaInformation": {
+ "maxLsa": 12000,
+ "maxLsaThreshold": 75,
+ },
+ "routerId": "1.1.1.1",
+ "lsaInformation": {
+ "lsaArrivalInterval": 1000,
+ "lsaStartInterval": 1000,
+ "lsaHoldInterval": 5000,
+ "lsaMaxWaitInterval": 5000,
+ "numLsa": 11500,
+ },
+ },
+ },
+ },
+ "TEST": {
+ "instList": {
+ "10": {
+ "instanceId": 10,
+ "maxLsaInformation": {
+ "maxLsa": 1000,
+ "maxLsaThreshold": 75,
+ },
+ "routerId": "20.20.20.20",
+ "lsaInformation": {
+ "lsaArrivalInterval": 1000,
+ "lsaStartInterval": 1000,
+ "lsaHoldInterval": 5000,
+ "lsaMaxWaitInterval": 5000,
+ "numLsa": 1500,
+ },
+ },
+ },
+ },
+ },
+ },
+ ],
+ "inputs": None,
+ "expected": {
+ "result": "failure",
+ "messages": ["OSPF Instances ['1', '10'] crossed the maximum LSA threshold."],
+ },
+ },
+ {
+ "name": "skipped",
+ "test": VerifyOSPFMaxLSA,
+ "eos_data": [
+ {
+ "vrfs": {},
+ },
+ ],
+ "inputs": None,
+ "expected": {"result": "skipped", "messages": ["No OSPF instance found."]},
+ },
]
diff --git a/tests/units/anta_tests/test_aaa.py b/tests/units/anta_tests/test_aaa.py
index 2992290..f0324c5 100644
--- a/tests/units/anta_tests/test_aaa.py
+++ b/tests/units/anta_tests/test_aaa.py
@@ -1,9 +1,8 @@
# 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.
-"""
-Tests for anta.tests.aaa.py
-"""
+"""Tests for anta.tests.aaa.py."""
+
from __future__ import annotations
from typing import Any
@@ -28,11 +27,11 @@ DATA: list[dict[str, Any]] = [
"tacacsServers": [
{
"serverInfo": {"hostname": "10.22.10.91", "authport": 49, "vrf": "MGMT"},
- }
+ },
],
"groups": {"GROUP1": {"serverGroup": "TACACS+", "members": [{"hostname": "SERVER1", "authport": 49, "vrf": "MGMT"}]}},
"srcIntf": {"MGMT": "Management0"},
- }
+ },
],
"inputs": {"intf": "Management0", "vrf": "MGMT"},
"expected": {"result": "success"},
@@ -45,7 +44,7 @@ DATA: list[dict[str, Any]] = [
"tacacsServers": [],
"groups": {},
"srcIntf": {},
- }
+ },
],
"inputs": {"intf": "Management0", "vrf": "MGMT"},
"expected": {"result": "failure", "messages": ["Source-interface Management0 is not configured in VRF MGMT"]},
@@ -58,11 +57,11 @@ DATA: list[dict[str, Any]] = [
"tacacsServers": [
{
"serverInfo": {"hostname": "10.22.10.91", "authport": 49, "vrf": "MGMT"},
- }
+ },
],
"groups": {"GROUP1": {"serverGroup": "TACACS+", "members": [{"hostname": "SERVER1", "authport": 49, "vrf": "MGMT"}]}},
"srcIntf": {"MGMT": "Management1"},
- }
+ },
],
"inputs": {"intf": "Management0", "vrf": "MGMT"},
"expected": {"result": "failure", "messages": ["Wrong source-interface configured in VRF MGMT"]},
@@ -75,11 +74,11 @@ DATA: list[dict[str, Any]] = [
"tacacsServers": [
{
"serverInfo": {"hostname": "10.22.10.91", "authport": 49, "vrf": "MGMT"},
- }
+ },
],
"groups": {"GROUP1": {"serverGroup": "TACACS+", "members": [{"hostname": "SERVER1", "authport": 49, "vrf": "MGMT"}]}},
"srcIntf": {"PROD": "Management0"},
- }
+ },
],
"inputs": {"intf": "Management0", "vrf": "MGMT"},
"expected": {"result": "failure", "messages": ["Source-interface Management0 is not configured in VRF MGMT"]},
@@ -92,11 +91,11 @@ DATA: list[dict[str, Any]] = [
"tacacsServers": [
{
"serverInfo": {"hostname": "10.22.10.91", "authport": 49, "vrf": "MGMT"},
- }
+ },
],
"groups": {"GROUP1": {"serverGroup": "TACACS+", "members": [{"hostname": "SERVER1", "authport": 49, "vrf": "MGMT"}]}},
"srcIntf": {"MGMT": "Management0"},
- }
+ },
],
"inputs": {"servers": ["10.22.10.91"], "vrf": "MGMT"},
"expected": {"result": "success"},
@@ -109,7 +108,7 @@ DATA: list[dict[str, Any]] = [
"tacacsServers": [],
"groups": {},
"srcIntf": {},
- }
+ },
],
"inputs": {"servers": ["10.22.10.91"], "vrf": "MGMT"},
"expected": {"result": "failure", "messages": ["No TACACS servers are configured"]},
@@ -122,11 +121,11 @@ DATA: list[dict[str, Any]] = [
"tacacsServers": [
{
"serverInfo": {"hostname": "10.22.10.91", "authport": 49, "vrf": "MGMT"},
- }
+ },
],
"groups": {"GROUP1": {"serverGroup": "TACACS+", "members": [{"hostname": "SERVER1", "authport": 49, "vrf": "MGMT"}]}},
"srcIntf": {"MGMT": "Management0"},
- }
+ },
],
"inputs": {"servers": ["10.22.10.91", "10.22.10.92"], "vrf": "MGMT"},
"expected": {"result": "failure", "messages": ["TACACS servers ['10.22.10.92'] are not configured in VRF MGMT"]},
@@ -139,11 +138,11 @@ DATA: list[dict[str, Any]] = [
"tacacsServers": [
{
"serverInfo": {"hostname": "10.22.10.91", "authport": 49, "vrf": "PROD"},
- }
+ },
],
"groups": {"GROUP1": {"serverGroup": "TACACS+", "members": [{"hostname": "SERVER1", "authport": 49, "vrf": "MGMT"}]}},
"srcIntf": {"MGMT": "Management0"},
- }
+ },
],
"inputs": {"servers": ["10.22.10.91"], "vrf": "MGMT"},
"expected": {"result": "failure", "messages": ["TACACS servers ['10.22.10.91'] are not configured in VRF MGMT"]},
@@ -156,11 +155,11 @@ DATA: list[dict[str, Any]] = [
"tacacsServers": [
{
"serverInfo": {"hostname": "10.22.10.91", "authport": 49, "vrf": "MGMT"},
- }
+ },
],
"groups": {"GROUP1": {"serverGroup": "TACACS+", "members": [{"hostname": "SERVER1", "authport": 49, "vrf": "MGMT"}]}},
"srcIntf": {"MGMT": "Management0"},
- }
+ },
],
"inputs": {"groups": ["GROUP1"]},
"expected": {"result": "success"},
@@ -173,7 +172,7 @@ DATA: list[dict[str, Any]] = [
"tacacsServers": [],
"groups": {},
"srcIntf": {},
- }
+ },
],
"inputs": {"groups": ["GROUP1"]},
"expected": {"result": "failure", "messages": ["No TACACS server group(s) are configured"]},
@@ -186,11 +185,11 @@ DATA: list[dict[str, Any]] = [
"tacacsServers": [
{
"serverInfo": {"hostname": "10.22.10.91", "authport": 49, "vrf": "MGMT"},
- }
+ },
],
"groups": {"GROUP2": {"serverGroup": "TACACS+", "members": [{"hostname": "SERVER1", "authport": 49, "vrf": "MGMT"}]}},
"srcIntf": {"MGMT": "Management0"},
- }
+ },
],
"inputs": {"groups": ["GROUP1"]},
"expected": {"result": "failure", "messages": ["TACACS server group(s) ['GROUP1'] are not configured"]},
@@ -203,7 +202,7 @@ DATA: list[dict[str, Any]] = [
"loginAuthenMethods": {"default": {"methods": ["group tacacs+", "local"]}, "login": {"methods": ["group tacacs+", "local"]}},
"enableAuthenMethods": {"default": {"methods": ["group tacacs+", "local"]}},
"dot1xAuthenMethods": {"default": {"methods": ["group radius"]}},
- }
+ },
],
"inputs": {"methods": ["tacacs+", "local"], "types": ["login", "enable"]},
"expected": {"result": "success"},
@@ -216,7 +215,7 @@ DATA: list[dict[str, Any]] = [
"loginAuthenMethods": {"default": {"methods": ["group tacacs+", "local"]}, "login": {"methods": ["group tacacs+", "local"]}},
"enableAuthenMethods": {"default": {"methods": ["group tacacs+", "local"]}},
"dot1xAuthenMethods": {"default": {"methods": ["group radius"]}},
- }
+ },
],
"inputs": {"methods": ["radius"], "types": ["dot1x"]},
"expected": {"result": "success"},
@@ -229,7 +228,7 @@ DATA: list[dict[str, Any]] = [
"loginAuthenMethods": {"default": {"methods": ["group tacacs+", "local"]}},
"enableAuthenMethods": {"default": {"methods": ["group tacacs+", "local"]}},
"dot1xAuthenMethods": {"default": {"methods": ["group radius"]}},
- }
+ },
],
"inputs": {"methods": ["tacacs+", "local"], "types": ["login", "enable"]},
"expected": {"result": "failure", "messages": ["AAA authentication methods are not configured for login console"]},
@@ -242,7 +241,7 @@ DATA: list[dict[str, Any]] = [
"loginAuthenMethods": {"default": {"methods": ["group tacacs+", "local"]}, "login": {"methods": ["group radius", "local"]}},
"enableAuthenMethods": {"default": {"methods": ["group tacacs+", "local"]}},
"dot1xAuthenMethods": {"default": {"methods": ["group radius"]}},
- }
+ },
],
"inputs": {"methods": ["tacacs+", "local"], "types": ["login", "enable"]},
"expected": {"result": "failure", "messages": ["AAA authentication methods ['group tacacs+', 'local'] are not matching for login console"]},
@@ -255,7 +254,7 @@ DATA: list[dict[str, Any]] = [
"loginAuthenMethods": {"default": {"methods": ["group radius", "local"]}, "login": {"methods": ["group tacacs+", "local"]}},
"enableAuthenMethods": {"default": {"methods": ["group tacacs+", "local"]}},
"dot1xAuthenMethods": {"default": {"methods": ["group radius"]}},
- }
+ },
],
"inputs": {"methods": ["tacacs+", "local"], "types": ["login", "enable"]},
"expected": {"result": "failure", "messages": ["AAA authentication methods ['group tacacs+', 'local'] are not matching for ['login']"]},
@@ -267,7 +266,7 @@ DATA: list[dict[str, Any]] = [
{
"commandsAuthzMethods": {"privilege0-15": {"methods": ["group tacacs+", "local"]}},
"execAuthzMethods": {"exec": {"methods": ["group tacacs+", "local"]}},
- }
+ },
],
"inputs": {"methods": ["tacacs+", "local"], "types": ["commands", "exec"]},
"expected": {"result": "success"},
@@ -279,7 +278,7 @@ DATA: list[dict[str, Any]] = [
{
"commandsAuthzMethods": {"privilege0-15": {"methods": ["group radius", "local"]}},
"execAuthzMethods": {"exec": {"methods": ["group tacacs+", "local"]}},
- }
+ },
],
"inputs": {"methods": ["tacacs+", "local"], "types": ["commands", "exec"]},
"expected": {"result": "failure", "messages": ["AAA authorization methods ['group tacacs+', 'local'] are not matching for ['commands']"]},
@@ -291,7 +290,7 @@ DATA: list[dict[str, Any]] = [
{
"commandsAuthzMethods": {"privilege0-15": {"methods": ["group tacacs+", "local"]}},
"execAuthzMethods": {"exec": {"methods": ["group radius", "local"]}},
- }
+ },
],
"inputs": {"methods": ["tacacs+", "local"], "types": ["commands", "exec"]},
"expected": {"result": "failure", "messages": ["AAA authorization methods ['group tacacs+', 'local'] are not matching for ['exec']"]},
@@ -305,7 +304,7 @@ DATA: list[dict[str, Any]] = [
"execAcctMethods": {"exec": {"defaultAction": "startStop", "defaultMethods": ["group tacacs+", "logging"], "consoleMethods": []}},
"systemAcctMethods": {"system": {"defaultAction": "startStop", "defaultMethods": ["group tacacs+", "logging"], "consoleMethods": []}},
"dot1xAcctMethods": {"dot1x": {"defaultMethods": [], "consoleMethods": []}},
- }
+ },
],
"inputs": {"methods": ["tacacs+", "logging"], "types": ["commands", "exec", "system"]},
"expected": {"result": "success"},
@@ -319,7 +318,7 @@ DATA: list[dict[str, Any]] = [
"execAcctMethods": {"exec": {"defaultAction": "startStop", "defaultMethods": ["group tacacs+", "logging"], "consoleMethods": []}},
"systemAcctMethods": {"system": {"defaultAction": "startStop", "defaultMethods": ["group tacacs+", "logging"], "consoleMethods": []}},
"dot1xAcctMethods": {"dot1x": {"defaultAction": "startStop", "defaultMethods": ["group radius", "logging"], "consoleMethods": []}},
- }
+ },
],
"inputs": {"methods": ["radius", "logging"], "types": ["dot1x"]},
"expected": {"result": "success"},
@@ -333,7 +332,7 @@ DATA: list[dict[str, Any]] = [
"execAcctMethods": {"exec": {"defaultAction": "startStop", "defaultMethods": ["group tacacs+", "logging"], "consoleMethods": []}},
"systemAcctMethods": {"system": {"defaultAction": "startStop", "defaultMethods": ["group tacacs+", "logging"], "consoleMethods": []}},
"dot1xAcctMethods": {"dot1x": {"defaultMethods": [], "consoleMethods": []}},
- }
+ },
],
"inputs": {"methods": ["tacacs+", "logging"], "types": ["commands", "exec", "system"]},
"expected": {"result": "failure", "messages": ["AAA default accounting is not configured for ['commands']"]},
@@ -347,7 +346,7 @@ DATA: list[dict[str, Any]] = [
"execAcctMethods": {"exec": {"defaultMethods": [], "consoleMethods": []}},
"commandsAcctMethods": {"privilege0-15": {"defaultMethods": [], "consoleMethods": []}},
"dot1xAcctMethods": {"dot1x": {"defaultMethods": [], "consoleMethods": []}},
- }
+ },
],
"inputs": {"methods": ["tacacs+", "logging"], "types": ["commands", "exec", "system"]},
"expected": {"result": "failure", "messages": ["AAA default accounting is not configured for ['system', 'exec', 'commands']"]},
@@ -361,7 +360,7 @@ DATA: list[dict[str, Any]] = [
"execAcctMethods": {"exec": {"defaultAction": "startStop", "defaultMethods": ["group tacacs+", "logging"], "consoleMethods": []}},
"systemAcctMethods": {"system": {"defaultAction": "startStop", "defaultMethods": ["group tacacs+", "logging"], "consoleMethods": []}},
"dot1xAcctMethods": {"dot1x": {"defaultMethods": [], "consoleMethods": []}},
- }
+ },
],
"inputs": {"methods": ["tacacs+", "logging"], "types": ["commands", "exec", "system"]},
"expected": {"result": "failure", "messages": ["AAA accounting default methods ['group tacacs+', 'logging'] are not matching for ['commands']"]},
@@ -376,24 +375,24 @@ DATA: list[dict[str, Any]] = [
"defaultMethods": [],
"consoleAction": "startStop",
"consoleMethods": ["group tacacs+", "logging"],
- }
+ },
},
"execAcctMethods": {
"exec": {
"defaultMethods": [],
"consoleAction": "startStop",
"consoleMethods": ["group tacacs+", "logging"],
- }
+ },
},
"systemAcctMethods": {
"system": {
"defaultMethods": [],
"consoleAction": "startStop",
"consoleMethods": ["group tacacs+", "logging"],
- }
+ },
},
"dot1xAcctMethods": {"dot1x": {"defaultMethods": [], "consoleMethods": []}},
- }
+ },
],
"inputs": {"methods": ["tacacs+", "logging"], "types": ["commands", "exec", "system"]},
"expected": {"result": "success"},
@@ -408,30 +407,30 @@ DATA: list[dict[str, Any]] = [
"defaultMethods": [],
"consoleAction": "startStop",
"consoleMethods": ["group tacacs+", "logging"],
- }
+ },
},
"execAcctMethods": {
"exec": {
"defaultMethods": [],
"consoleAction": "startStop",
"consoleMethods": ["group tacacs+", "logging"],
- }
+ },
},
"systemAcctMethods": {
"system": {
"defaultMethods": [],
"consoleAction": "startStop",
"consoleMethods": ["group tacacs+", "logging"],
- }
+ },
},
"dot1xAcctMethods": {
"dot1x": {
"defaultMethods": [],
"consoleAction": "startStop",
"consoleMethods": ["group tacacs+", "logging"],
- }
+ },
},
- }
+ },
],
"inputs": {"methods": ["tacacs+", "logging"], "types": ["dot1x"]},
"expected": {"result": "success"},
@@ -445,24 +444,24 @@ DATA: list[dict[str, Any]] = [
"privilege0-15": {
"defaultMethods": [],
"consoleMethods": [],
- }
+ },
},
"execAcctMethods": {
"exec": {
"defaultMethods": [],
"consoleAction": "startStop",
"consoleMethods": ["group tacacs+", "logging"],
- }
+ },
},
"systemAcctMethods": {
"system": {
"defaultMethods": [],
"consoleAction": "startStop",
"consoleMethods": ["group tacacs+", "logging"],
- }
+ },
},
"dot1xAcctMethods": {"dot1x": {"defaultMethods": [], "consoleMethods": []}},
- }
+ },
],
"inputs": {"methods": ["tacacs+", "logging"], "types": ["commands", "exec", "system"]},
"expected": {"result": "failure", "messages": ["AAA console accounting is not configured for ['commands']"]},
@@ -476,7 +475,7 @@ DATA: list[dict[str, Any]] = [
"execAcctMethods": {"exec": {"defaultMethods": [], "consoleMethods": []}},
"commandsAcctMethods": {"privilege0-15": {"defaultMethods": [], "consoleMethods": []}},
"dot1xAcctMethods": {"dot1x": {"defaultMethods": [], "consoleMethods": []}},
- }
+ },
],
"inputs": {"methods": ["tacacs+", "logging"], "types": ["commands", "exec", "system"]},
"expected": {"result": "failure", "messages": ["AAA console accounting is not configured for ['system', 'exec', 'commands']"]},
@@ -491,24 +490,24 @@ DATA: list[dict[str, Any]] = [
"defaultMethods": [],
"consoleAction": "startStop",
"consoleMethods": ["group radius", "logging"],
- }
+ },
},
"execAcctMethods": {
"exec": {
"defaultMethods": [],
"consoleAction": "startStop",
"consoleMethods": ["group tacacs+", "logging"],
- }
+ },
},
"systemAcctMethods": {
"system": {
"defaultMethods": [],
"consoleAction": "startStop",
"consoleMethods": ["group tacacs+", "logging"],
- }
+ },
},
"dot1xAcctMethods": {"dot1x": {"defaultMethods": [], "consoleMethods": []}},
- }
+ },
],
"inputs": {"methods": ["tacacs+", "logging"], "types": ["commands", "exec", "system"]},
"expected": {"result": "failure", "messages": ["AAA accounting console methods ['group tacacs+', 'logging'] are not matching for ['commands']"]},
diff --git a/tests/units/anta_tests/test_bfd.py b/tests/units/anta_tests/test_bfd.py
index 67bb0b4..54dc7a0 100644
--- a/tests/units/anta_tests/test_bfd.py
+++ b/tests/units/anta_tests/test_bfd.py
@@ -1,9 +1,8 @@
# 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.
-"""
-Tests for anta.tests.bfd.py
-"""
+"""Tests for anta.tests.bfd.py."""
+
# pylint: disable=C0302
from __future__ import annotations
@@ -11,7 +10,7 @@ from typing import Any
# pylint: disable=C0413
# because of the patch above
-from anta.tests.bfd import VerifyBFDPeersHealth, VerifyBFDPeersIntervals, VerifyBFDSpecificPeers # noqa: E402
+from anta.tests.bfd import VerifyBFDPeersHealth, VerifyBFDPeersIntervals, VerifyBFDSpecificPeers
from tests.lib.anta import test # noqa: F401; pylint: disable=W0611
DATA: list[dict[str, Any]] = [
diff --git a/tests/units/anta_tests/test_configuration.py b/tests/units/anta_tests/test_configuration.py
index a2ab673..0444db6 100644
--- a/tests/units/anta_tests/test_configuration.py
+++ b/tests/units/anta_tests/test_configuration.py
@@ -1,7 +1,8 @@
# 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.
-"""Data for testing anta.tests.configuration"""
+"""Data for testing anta.tests.configuration."""
+
from __future__ import annotations
from typing import Any
diff --git a/tests/units/anta_tests/test_connectivity.py b/tests/units/anta_tests/test_connectivity.py
index f79ce24..bd30811 100644
--- a/tests/units/anta_tests/test_connectivity.py
+++ b/tests/units/anta_tests/test_connectivity.py
@@ -1,9 +1,8 @@
# 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.
-"""
-Tests for anta.tests.connectivity.py
-"""
+"""Tests for anta.tests.connectivity.py."""
+
from __future__ import annotations
from typing import Any
@@ -27,8 +26,8 @@ DATA: list[dict[str, Any]] = [
2 packets transmitted, 2 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.072/0.159/0.247/0.088 ms, ipg/ewma 0.370/0.225 ms
- """
- ]
+ """,
+ ],
},
{
"messages": [
@@ -40,8 +39,8 @@ DATA: list[dict[str, Any]] = [
2 packets transmitted, 2 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.072/0.159/0.247/0.088 ms, ipg/ewma 0.370/0.225 ms
- """
- ]
+ """,
+ ],
},
],
"expected": {"result": "success"},
@@ -61,8 +60,8 @@ DATA: list[dict[str, Any]] = [
2 packets transmitted, 2 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.072/0.159/0.247/0.088 ms, ipg/ewma 0.370/0.225 ms
- """
- ]
+ """,
+ ],
},
{
"messages": [
@@ -74,8 +73,8 @@ DATA: list[dict[str, Any]] = [
2 packets transmitted, 2 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.072/0.159/0.247/0.088 ms, ipg/ewma 0.370/0.225 ms
- """
- ]
+ """,
+ ],
},
],
"expected": {"result": "success"},
@@ -94,8 +93,8 @@ DATA: list[dict[str, Any]] = [
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.072/0.159/0.247/0.088 ms, ipg/ewma 0.370/0.225 ms
- """
- ]
+ """,
+ ],
},
],
"expected": {"result": "success"},
@@ -115,8 +114,8 @@ DATA: list[dict[str, Any]] = [
2 packets transmitted, 0 received, 100% packet loss, time 10ms
- """
- ]
+ """,
+ ],
},
{
"messages": [
@@ -128,8 +127,8 @@ DATA: list[dict[str, Any]] = [
2 packets transmitted, 2 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.072/0.159/0.247/0.088 ms, ipg/ewma 0.370/0.225 ms
- """
- ]
+ """,
+ ],
},
],
"expected": {"result": "failure", "messages": ["Connectivity test failed for the following source-destination pairs: [('10.0.0.5', '10.0.0.11')]"]},
@@ -149,8 +148,8 @@ DATA: list[dict[str, Any]] = [
2 packets transmitted, 0 received, 100% packet loss, time 10ms
- """
- ]
+ """,
+ ],
},
{
"messages": [
@@ -162,8 +161,8 @@ DATA: list[dict[str, Any]] = [
2 packets transmitted, 2 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.072/0.159/0.247/0.088 ms, ipg/ewma 0.370/0.225 ms
- """
- ]
+ """,
+ ],
},
],
"expected": {"result": "failure", "messages": ["Connectivity test failed for the following source-destination pairs: [('Management0', '10.0.0.11')]"]},
@@ -175,7 +174,7 @@ DATA: list[dict[str, Any]] = [
"neighbors": [
{"port": "Ethernet1", "neighbor_device": "DC1-SPINE1", "neighbor_port": "Ethernet1"},
{"port": "Ethernet2", "neighbor_device": "DC1-SPINE2", "neighbor_port": "Ethernet1"},
- ]
+ ],
},
"eos_data": [
{
@@ -192,8 +191,8 @@ DATA: list[dict[str, Any]] = [
"interfaceId_v2": "Ethernet1",
"interfaceDescription": "P2P_LINK_TO_DC1-LEAF1A_Ethernet1",
},
- }
- ]
+ },
+ ],
},
"Ethernet2": {
"lldpNeighborInfo": [
@@ -207,11 +206,53 @@ DATA: list[dict[str, Any]] = [
"interfaceId_v2": "Ethernet1",
"interfaceDescription": "P2P_LINK_TO_DC1-LEAF1A_Ethernet2",
},
- }
- ]
+ },
+ ],
},
- }
- }
+ },
+ },
+ ],
+ "expected": {"result": "success"},
+ },
+ {
+ "name": "success-multiple-neighbors",
+ "test": VerifyLLDPNeighbors,
+ "inputs": {
+ "neighbors": [
+ {"port": "Ethernet1", "neighbor_device": "DC1-SPINE2", "neighbor_port": "Ethernet1"},
+ ],
+ },
+ "eos_data": [
+ {
+ "lldpNeighbors": {
+ "Ethernet1": {
+ "lldpNeighborInfo": [
+ {
+ "chassisIdType": "macAddress",
+ "chassisId": "001c.73a0.fc18",
+ "systemName": "DC1-SPINE1",
+ "neighborInterfaceInfo": {
+ "interfaceIdType": "interfaceName",
+ "interfaceId": '"Ethernet1"',
+ "interfaceId_v2": "Ethernet1",
+ "interfaceDescription": "P2P_LINK_TO_DC1-LEAF1A_Ethernet1",
+ },
+ },
+ {
+ "chassisIdType": "macAddress",
+ "chassisId": "001c.73f7.d138",
+ "systemName": "DC1-SPINE2",
+ "neighborInterfaceInfo": {
+ "interfaceIdType": "interfaceName",
+ "interfaceId": '"Ethernet1"',
+ "interfaceId_v2": "Ethernet1",
+ "interfaceDescription": "P2P_LINK_TO_DC1-LEAF1A_Ethernet2",
+ },
+ },
+ ],
+ },
+ },
+ },
],
"expected": {"result": "success"},
},
@@ -222,7 +263,7 @@ DATA: list[dict[str, Any]] = [
"neighbors": [
{"port": "Ethernet1", "neighbor_device": "DC1-SPINE1", "neighbor_port": "Ethernet1"},
{"port": "Ethernet2", "neighbor_device": "DC1-SPINE2", "neighbor_port": "Ethernet1"},
- ]
+ ],
},
"eos_data": [
{
@@ -239,13 +280,13 @@ DATA: list[dict[str, Any]] = [
"interfaceId_v2": "Ethernet1",
"interfaceDescription": "P2P_LINK_TO_DC1-LEAF1A_Ethernet1",
},
- }
- ]
+ },
+ ],
},
- }
- }
+ },
+ },
],
- "expected": {"result": "failure", "messages": ["The following port(s) have issues: {'port_not_configured': ['Ethernet2']}"]},
+ "expected": {"result": "failure", "messages": ["Port(s) not configured:\n Ethernet2"]},
},
{
"name": "failure-no-neighbor",
@@ -254,7 +295,7 @@ DATA: list[dict[str, Any]] = [
"neighbors": [
{"port": "Ethernet1", "neighbor_device": "DC1-SPINE1", "neighbor_port": "Ethernet1"},
{"port": "Ethernet2", "neighbor_device": "DC1-SPINE2", "neighbor_port": "Ethernet1"},
- ]
+ ],
},
"eos_data": [
{
@@ -271,14 +312,14 @@ DATA: list[dict[str, Any]] = [
"interfaceId_v2": "Ethernet1",
"interfaceDescription": "P2P_LINK_TO_DC1-LEAF1A_Ethernet1",
},
- }
- ]
+ },
+ ],
},
"Ethernet2": {"lldpNeighborInfo": []},
- }
- }
+ },
+ },
],
- "expected": {"result": "failure", "messages": ["The following port(s) have issues: {'no_lldp_neighbor': ['Ethernet2']}"]},
+ "expected": {"result": "failure", "messages": ["No LLDP neighbor(s) on port(s):\n Ethernet2"]},
},
{
"name": "failure-wrong-neighbor",
@@ -287,7 +328,7 @@ DATA: list[dict[str, Any]] = [
"neighbors": [
{"port": "Ethernet1", "neighbor_device": "DC1-SPINE1", "neighbor_port": "Ethernet1"},
{"port": "Ethernet2", "neighbor_device": "DC1-SPINE2", "neighbor_port": "Ethernet1"},
- ]
+ ],
},
"eos_data": [
{
@@ -304,8 +345,8 @@ DATA: list[dict[str, Any]] = [
"interfaceId_v2": "Ethernet1",
"interfaceDescription": "P2P_LINK_TO_DC1-LEAF1A_Ethernet1",
},
- }
- ]
+ },
+ ],
},
"Ethernet2": {
"lldpNeighborInfo": [
@@ -319,13 +360,13 @@ DATA: list[dict[str, Any]] = [
"interfaceId_v2": "Ethernet2",
"interfaceDescription": "P2P_LINK_TO_DC1-LEAF1A_Ethernet2",
},
- }
- ]
+ },
+ ],
},
- }
- }
+ },
+ },
],
- "expected": {"result": "failure", "messages": ["The following port(s) have issues: {'wrong_lldp_neighbor': ['Ethernet2']}"]},
+ "expected": {"result": "failure", "messages": ["Wrong LLDP neighbor(s) on port(s):\n Ethernet2\n DC1-SPINE2_Ethernet2"]},
},
{
"name": "failure-multiple",
@@ -335,7 +376,7 @@ DATA: list[dict[str, Any]] = [
{"port": "Ethernet1", "neighbor_device": "DC1-SPINE1", "neighbor_port": "Ethernet1"},
{"port": "Ethernet2", "neighbor_device": "DC1-SPINE2", "neighbor_port": "Ethernet1"},
{"port": "Ethernet3", "neighbor_device": "DC1-SPINE3", "neighbor_port": "Ethernet1"},
- ]
+ ],
},
"eos_data": [
{
@@ -352,18 +393,62 @@ DATA: list[dict[str, Any]] = [
"interfaceId_v2": "Ethernet2",
"interfaceDescription": "P2P_LINK_TO_DC1-LEAF1A_Ethernet1",
},
- }
- ]
+ },
+ ],
},
"Ethernet2": {"lldpNeighborInfo": []},
- }
- }
+ },
+ },
],
"expected": {
"result": "failure",
"messages": [
- "The following port(s) have issues: {'wrong_lldp_neighbor': ['Ethernet1'], 'no_lldp_neighbor': ['Ethernet2'], 'port_not_configured': ['Ethernet3']}"
+ "Wrong LLDP neighbor(s) on port(s):\n Ethernet1\n DC1-SPINE1_Ethernet2\n"
+ "No LLDP neighbor(s) on port(s):\n Ethernet2\n"
+ "Port(s) not configured:\n Ethernet3"
],
},
},
+ {
+ "name": "failure-multiple-neighbors",
+ "test": VerifyLLDPNeighbors,
+ "inputs": {
+ "neighbors": [
+ {"port": "Ethernet1", "neighbor_device": "DC1-SPINE3", "neighbor_port": "Ethernet1"},
+ ],
+ },
+ "eos_data": [
+ {
+ "lldpNeighbors": {
+ "Ethernet1": {
+ "lldpNeighborInfo": [
+ {
+ "chassisIdType": "macAddress",
+ "chassisId": "001c.73a0.fc18",
+ "systemName": "DC1-SPINE1",
+ "neighborInterfaceInfo": {
+ "interfaceIdType": "interfaceName",
+ "interfaceId": '"Ethernet1"',
+ "interfaceId_v2": "Ethernet1",
+ "interfaceDescription": "P2P_LINK_TO_DC1-LEAF1A_Ethernet1",
+ },
+ },
+ {
+ "chassisIdType": "macAddress",
+ "chassisId": "001c.73f7.d138",
+ "systemName": "DC1-SPINE2",
+ "neighborInterfaceInfo": {
+ "interfaceIdType": "interfaceName",
+ "interfaceId": '"Ethernet1"',
+ "interfaceId_v2": "Ethernet1",
+ "interfaceDescription": "P2P_LINK_TO_DC1-LEAF1A_Ethernet2",
+ },
+ },
+ ],
+ },
+ },
+ },
+ ],
+ "expected": {"result": "failure", "messages": ["Wrong LLDP neighbor(s) on port(s):\n Ethernet1\n DC1-SPINE1_Ethernet1\n DC1-SPINE2_Ethernet1"]},
+ },
]
diff --git a/tests/units/anta_tests/test_field_notices.py b/tests/units/anta_tests/test_field_notices.py
index 7c17f22..66e7801 100644
--- a/tests/units/anta_tests/test_field_notices.py
+++ b/tests/units/anta_tests/test_field_notices.py
@@ -1,7 +1,8 @@
# 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 inputs for anta.tests.field_notices"""
+"""Test inputs for anta.tests.field_notices."""
+
from __future__ import annotations
from typing import Any
@@ -22,7 +23,7 @@ DATA: list[dict[str, Any]] = [
"deviations": [],
"components": [{"name": "Aboot", "version": "Aboot-veos-8.0.0-3255441"}],
},
- }
+ },
],
"inputs": None,
"expected": {"result": "success"},
@@ -39,10 +40,13 @@ DATA: list[dict[str, Any]] = [
"deviations": [],
"components": [{"name": "Aboot", "version": "Aboot-veos-4.0.1-3255441"}],
},
- }
+ },
],
"inputs": None,
- "expected": {"result": "failure", "messages": ["device is running incorrect version of aboot (4.0.1)"]},
+ "expected": {
+ "result": "failure",
+ "messages": ["device is running incorrect version of aboot (4.0.1)"],
+ },
},
{
"name": "failure-4.1",
@@ -56,10 +60,13 @@ DATA: list[dict[str, Any]] = [
"deviations": [],
"components": [{"name": "Aboot", "version": "Aboot-veos-4.1.0-3255441"}],
},
- }
+ },
],
"inputs": None,
- "expected": {"result": "failure", "messages": ["device is running incorrect version of aboot (4.1.0)"]},
+ "expected": {
+ "result": "failure",
+ "messages": ["device is running incorrect version of aboot (4.1.0)"],
+ },
},
{
"name": "failure-6.0",
@@ -73,10 +80,13 @@ DATA: list[dict[str, Any]] = [
"deviations": [],
"components": [{"name": "Aboot", "version": "Aboot-veos-6.0.1-3255441"}],
},
- }
+ },
],
"inputs": None,
- "expected": {"result": "failure", "messages": ["device is running incorrect version of aboot (6.0.1)"]},
+ "expected": {
+ "result": "failure",
+ "messages": ["device is running incorrect version of aboot (6.0.1)"],
+ },
},
{
"name": "failure-6.1",
@@ -90,10 +100,13 @@ DATA: list[dict[str, Any]] = [
"deviations": [],
"components": [{"name": "Aboot", "version": "Aboot-veos-6.1.1-3255441"}],
},
- }
+ },
],
"inputs": None,
- "expected": {"result": "failure", "messages": ["device is running incorrect version of aboot (6.1.1)"]},
+ "expected": {
+ "result": "failure",
+ "messages": ["device is running incorrect version of aboot (6.1.1)"],
+ },
},
{
"name": "skipped-model",
@@ -107,10 +120,13 @@ DATA: list[dict[str, Any]] = [
"deviations": [],
"components": [{"name": "Aboot", "version": "Aboot-veos-8.0.0-3255441"}],
},
- }
+ },
],
"inputs": None,
- "expected": {"result": "skipped", "messages": ["device is not impacted by FN044"]},
+ "expected": {
+ "result": "skipped",
+ "messages": ["device is not impacted by FN044"],
+ },
},
{
"name": "success-JPE",
@@ -123,7 +139,7 @@ DATA: list[dict[str, Any]] = [
"deviations": [],
"components": [{"name": "FixedSystemvrm1", "version": "7"}],
},
- }
+ },
],
"inputs": None,
"expected": {"result": "success", "messages": ["FN72 is mitigated"]},
@@ -139,7 +155,7 @@ DATA: list[dict[str, Any]] = [
"deviations": [],
"components": [{"name": "FixedSystemvrm1", "version": "7"}],
},
- }
+ },
],
"inputs": None,
"expected": {"result": "success", "messages": ["FN72 is mitigated"]},
@@ -155,7 +171,7 @@ DATA: list[dict[str, Any]] = [
"deviations": [],
"components": [{"name": "FixedSystemvrm1", "version": "7"}],
},
- }
+ },
],
"inputs": None,
"expected": {"result": "success", "messages": ["FN72 is mitigated"]},
@@ -171,7 +187,7 @@ DATA: list[dict[str, Any]] = [
"deviations": [],
"components": [{"name": "FixedSystemvrm1", "version": "7"}],
},
- }
+ },
],
"inputs": None,
"expected": {"result": "success", "messages": ["FN72 is mitigated"]},
@@ -187,7 +203,7 @@ DATA: list[dict[str, Any]] = [
"deviations": [],
"components": [{"name": "FixedSystemvrm1", "version": "7"}],
},
- }
+ },
],
"inputs": None,
"expected": {"result": "skipped", "messages": ["Device not exposed"]},
@@ -203,10 +219,13 @@ DATA: list[dict[str, Any]] = [
"deviations": [],
"components": [{"name": "FixedSystemvrm1", "version": "5"}],
},
- }
+ },
],
"inputs": None,
- "expected": {"result": "skipped", "messages": ["Platform is not impacted by FN072"]},
+ "expected": {
+ "result": "skipped",
+ "messages": ["Platform is not impacted by FN072"],
+ },
},
{
"name": "skipped-range-JPE",
@@ -219,7 +238,39 @@ DATA: list[dict[str, Any]] = [
"deviations": [],
"components": [{"name": "FixedSystemvrm1", "version": "5"}],
},
- }
+ },
+ ],
+ "inputs": None,
+ "expected": {"result": "skipped", "messages": ["Device not exposed"]},
+ },
+ {
+ "name": "skipped-range-K-JPE",
+ "test": VerifyFieldNotice72Resolution,
+ "eos_data": [
+ {
+ "modelName": "DCS-7280SR3K-48YC8",
+ "serialNumber": "JPE2134000",
+ "details": {
+ "deviations": [],
+ "components": [{"name": "FixedSystemvrm1", "version": "5"}],
+ },
+ },
+ ],
+ "inputs": None,
+ "expected": {"result": "skipped", "messages": ["Device not exposed"]},
+ },
+ {
+ "name": "skipped-range-JAS",
+ "test": VerifyFieldNotice72Resolution,
+ "eos_data": [
+ {
+ "modelName": "DCS-7280SR3-48YC8",
+ "serialNumber": "JAS2041000",
+ "details": {
+ "deviations": [],
+ "components": [{"name": "FixedSystemvrm1", "version": "5"}],
+ },
+ },
],
"inputs": None,
"expected": {"result": "skipped", "messages": ["Device not exposed"]},
@@ -235,7 +286,7 @@ DATA: list[dict[str, Any]] = [
"deviations": [],
"components": [{"name": "FixedSystemvrm1", "version": "5"}],
},
- }
+ },
],
"inputs": None,
"expected": {"result": "skipped", "messages": ["Device not exposed"]},
@@ -251,7 +302,7 @@ DATA: list[dict[str, Any]] = [
"deviations": [],
"components": [{"name": "FixedSystemvrm1", "version": "5"}],
},
- }
+ },
],
"inputs": None,
"expected": {"result": "failure", "messages": ["Device is exposed to FN72"]},
@@ -267,7 +318,7 @@ DATA: list[dict[str, Any]] = [
"deviations": [],
"components": [{"name": "FixedSystemvrm1", "version": "5"}],
},
- }
+ },
],
"inputs": None,
"expected": {"result": "failure", "messages": ["Device is exposed to FN72"]},
@@ -283,9 +334,12 @@ DATA: list[dict[str, Any]] = [
"deviations": [],
"components": [{"name": "FixedSystemvrm2", "version": "5"}],
},
- }
+ },
],
"inputs": None,
- "expected": {"result": "error", "messages": ["Error in running test - FixedSystemvrm1 not found"]},
+ "expected": {
+ "result": "error",
+ "messages": ["Error in running test - FixedSystemvrm1 not found"],
+ },
},
]
diff --git a/tests/units/anta_tests/test_greent.py b/tests/units/anta_tests/test_greent.py
index 65789a2..2c48301 100644
--- a/tests/units/anta_tests/test_greent.py
+++ b/tests/units/anta_tests/test_greent.py
@@ -1,12 +1,14 @@
# 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.
-"""Data for testing anta.tests.configuration"""
+"""Data for testing anta.tests.configuration."""
+
from __future__ import annotations
from typing import Any
from anta.tests.greent import VerifyGreenT, VerifyGreenTCounters
+from tests.lib.anta import test # noqa: F401; pylint: disable=W0611
DATA: list[dict[str, Any]] = [
{
@@ -21,12 +23,19 @@ DATA: list[dict[str, Any]] = [
"test": VerifyGreenTCounters,
"eos_data": [{"sampleRcvd": 0, "sampleDiscarded": 0, "multiDstSampleRcvd": 0, "grePktSent": 0, "sampleSent": 0}],
"inputs": None,
- "expected": {"result": "failure"},
+ "expected": {"result": "failure", "messages": ["GreenT counters are not incremented"]},
},
{
"name": "success",
"test": VerifyGreenT,
- "eos_data": [{"sampleRcvd": 0, "sampleDiscarded": 0, "multiDstSampleRcvd": 0, "grePktSent": 1, "sampleSent": 0}],
+ "eos_data": [
+ {
+ "profiles": {
+ "default": {"interfaces": [], "appliedInterfaces": [], "samplePolicy": "default", "failures": {}, "appliedInterfaces6": [], "failures6": {}},
+ "testProfile": {"interfaces": [], "appliedInterfaces": [], "samplePolicy": "default", "failures": {}, "appliedInterfaces6": [], "failures6": {}},
+ },
+ },
+ ],
"inputs": None,
"expected": {"result": "success"},
},
@@ -37,11 +46,10 @@ DATA: list[dict[str, Any]] = [
{
"profiles": {
"default": {"interfaces": [], "appliedInterfaces": [], "samplePolicy": "default", "failures": {}, "appliedInterfaces6": [], "failures6": {}},
- "testProfile": {"interfaces": [], "appliedInterfaces": [], "samplePolicy": "default", "failures": {}, "appliedInterfaces6": [], "failures6": {}},
- }
- }
+ },
+ },
],
"inputs": None,
- "expected": {"result": "failure"},
+ "expected": {"result": "failure", "messages": ["No GreenT policy is created"]},
},
]
diff --git a/tests/units/anta_tests/test_hardware.py b/tests/units/anta_tests/test_hardware.py
index 5279d89..e601c68 100644
--- a/tests/units/anta_tests/test_hardware.py
+++ b/tests/units/anta_tests/test_hardware.py
@@ -1,7 +1,8 @@
# 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 inputs for anta.tests.hardware"""
+"""Test inputs for anta.tests.hardware."""
+
from __future__ import annotations
from typing import Any
@@ -26,8 +27,8 @@ DATA: list[dict[str, Any]] = [
"xcvrSlots": {
"1": {"mfgName": "Arista Networks", "modelName": "QSFP-100G-DR", "serialNum": "XKT203501340", "hardwareRev": "21"},
"2": {"mfgName": "Arista Networks", "modelName": "QSFP-100G-DR", "serialNum": "XKT203501337", "hardwareRev": "21"},
- }
- }
+ },
+ },
],
"inputs": {"manufacturers": ["Arista Networks"]},
"expected": {"result": "success"},
@@ -40,8 +41,8 @@ DATA: list[dict[str, Any]] = [
"xcvrSlots": {
"1": {"mfgName": "Arista Networks", "modelName": "QSFP-100G-DR", "serialNum": "XKT203501340", "hardwareRev": "21"},
"2": {"mfgName": "Arista Networks", "modelName": "QSFP-100G-DR", "serialNum": "XKT203501337", "hardwareRev": "21"},
- }
- }
+ },
+ },
],
"inputs": {"manufacturers": ["Arista"]},
"expected": {"result": "failure", "messages": ["Some transceivers are from unapproved manufacturers: {'1': 'Arista Networks', '2': 'Arista Networks'}"]},
@@ -57,7 +58,7 @@ DATA: list[dict[str, Any]] = [
"shutdownOnOverheat": "True",
"systemStatus": "temperatureOk",
"recoveryModeOnOverheat": "recoveryModeNA",
- }
+ },
],
"inputs": None,
"expected": {"result": "success"},
@@ -73,7 +74,7 @@ DATA: list[dict[str, Any]] = [
"shutdownOnOverheat": "True",
"systemStatus": "temperatureKO",
"recoveryModeOnOverheat": "recoveryModeNA",
- }
+ },
],
"inputs": None,
"expected": {"result": "failure", "messages": ["Device temperature exceeds acceptable limits. Current system status: 'temperatureKO'"]},
@@ -100,10 +101,10 @@ DATA: list[dict[str, Any]] = [
"pidDriverCount": 0,
"isPidDriver": False,
"name": "DomTemperatureSensor54",
- }
+ },
],
"cardSlots": [],
- }
+ },
],
"inputs": None,
"expected": {"result": "success"},
@@ -130,10 +131,10 @@ DATA: list[dict[str, Any]] = [
"pidDriverCount": 0,
"isPidDriver": False,
"name": "DomTemperatureSensor54",
- }
+ },
],
"cardSlots": [],
- }
+ },
],
"inputs": None,
"expected": {
@@ -141,7 +142,7 @@ DATA: list[dict[str, Any]] = [
"messages": [
"The following sensors are operating outside the acceptable temperature range or have raised alerts: "
"{'DomTemperatureSensor54': "
- "{'hwStatus': 'ko', 'alertCount': 0}}"
+ "{'hwStatus': 'ko', 'alertCount': 0}}",
],
},
},
@@ -167,10 +168,10 @@ DATA: list[dict[str, Any]] = [
"pidDriverCount": 0,
"isPidDriver": False,
"name": "DomTemperatureSensor54",
- }
+ },
],
"cardSlots": [],
- }
+ },
],
"inputs": None,
"expected": {
@@ -178,7 +179,7 @@ DATA: list[dict[str, Any]] = [
"messages": [
"The following sensors are operating outside the acceptable temperature range or have raised alerts: "
"{'DomTemperatureSensor54': "
- "{'hwStatus': 'ok', 'alertCount': 1}}"
+ "{'hwStatus': 'ok', 'alertCount': 1}}",
],
},
},
@@ -200,7 +201,7 @@ DATA: list[dict[str, Any]] = [
"currentZones": 1,
"configuredZones": 0,
"systemStatus": "coolingOk",
- }
+ },
],
"inputs": None,
"expected": {"result": "success"},
@@ -223,7 +224,7 @@ DATA: list[dict[str, Any]] = [
"currentZones": 1,
"configuredZones": 0,
"systemStatus": "coolingKo",
- }
+ },
],
"inputs": None,
"expected": {"result": "failure", "messages": ["Device system cooling is not OK: 'coolingKo'"]},
@@ -254,7 +255,7 @@ DATA: list[dict[str, Any]] = [
"speedHwOverride": True,
"speedStable": True,
"label": "PowerSupply1/1",
- }
+ },
],
"speed": 30,
"label": "PowerSupply1",
@@ -272,7 +273,7 @@ DATA: list[dict[str, Any]] = [
"speedHwOverride": True,
"speedStable": True,
"label": "PowerSupply2/1",
- }
+ },
],
"speed": 30,
"label": "PowerSupply2",
@@ -292,7 +293,7 @@ DATA: list[dict[str, Any]] = [
"speedHwOverride": False,
"speedStable": True,
"label": "1/1",
- }
+ },
],
"speed": 30,
"label": "1",
@@ -310,7 +311,7 @@ DATA: list[dict[str, Any]] = [
"speedHwOverride": False,
"speedStable": True,
"label": "2/1",
- }
+ },
],
"speed": 30,
"label": "2",
@@ -328,7 +329,7 @@ DATA: list[dict[str, Any]] = [
"speedHwOverride": False,
"speedStable": True,
"label": "3/1",
- }
+ },
],
"speed": 30,
"label": "3",
@@ -346,7 +347,7 @@ DATA: list[dict[str, Any]] = [
"speedHwOverride": False,
"speedStable": True,
"label": "4/1",
- }
+ },
],
"speed": 30,
"label": "4",
@@ -356,7 +357,7 @@ DATA: list[dict[str, Any]] = [
"currentZones": 1,
"configuredZones": 0,
"systemStatus": "coolingOk",
- }
+ },
],
"inputs": {"states": ["ok"]},
"expected": {"result": "success"},
@@ -387,7 +388,7 @@ DATA: list[dict[str, Any]] = [
"speedHwOverride": True,
"speedStable": True,
"label": "PowerSupply1/1",
- }
+ },
],
"speed": 30,
"label": "PowerSupply1",
@@ -405,7 +406,7 @@ DATA: list[dict[str, Any]] = [
"speedHwOverride": True,
"speedStable": True,
"label": "PowerSupply2/1",
- }
+ },
],
"speed": 30,
"label": "PowerSupply2",
@@ -425,7 +426,7 @@ DATA: list[dict[str, Any]] = [
"speedHwOverride": False,
"speedStable": True,
"label": "1/1",
- }
+ },
],
"speed": 30,
"label": "1",
@@ -443,7 +444,7 @@ DATA: list[dict[str, Any]] = [
"speedHwOverride": False,
"speedStable": True,
"label": "2/1",
- }
+ },
],
"speed": 30,
"label": "2",
@@ -461,7 +462,7 @@ DATA: list[dict[str, Any]] = [
"speedHwOverride": False,
"speedStable": True,
"label": "3/1",
- }
+ },
],
"speed": 30,
"label": "3",
@@ -479,7 +480,7 @@ DATA: list[dict[str, Any]] = [
"speedHwOverride": False,
"speedStable": True,
"label": "4/1",
- }
+ },
],
"speed": 30,
"label": "4",
@@ -489,7 +490,7 @@ DATA: list[dict[str, Any]] = [
"currentZones": 1,
"configuredZones": 0,
"systemStatus": "coolingOk",
- }
+ },
],
"inputs": {"states": ["ok", "Not Inserted"]},
"expected": {"result": "success"},
@@ -520,7 +521,7 @@ DATA: list[dict[str, Any]] = [
"speedHwOverride": True,
"speedStable": True,
"label": "PowerSupply1/1",
- }
+ },
],
"speed": 30,
"label": "PowerSupply1",
@@ -538,7 +539,7 @@ DATA: list[dict[str, Any]] = [
"speedHwOverride": True,
"speedStable": True,
"label": "PowerSupply2/1",
- }
+ },
],
"speed": 30,
"label": "PowerSupply2",
@@ -558,7 +559,7 @@ DATA: list[dict[str, Any]] = [
"speedHwOverride": False,
"speedStable": True,
"label": "1/1",
- }
+ },
],
"speed": 30,
"label": "1",
@@ -576,7 +577,7 @@ DATA: list[dict[str, Any]] = [
"speedHwOverride": False,
"speedStable": True,
"label": "2/1",
- }
+ },
],
"speed": 30,
"label": "2",
@@ -594,7 +595,7 @@ DATA: list[dict[str, Any]] = [
"speedHwOverride": False,
"speedStable": True,
"label": "3/1",
- }
+ },
],
"speed": 30,
"label": "3",
@@ -612,7 +613,7 @@ DATA: list[dict[str, Any]] = [
"speedHwOverride": False,
"speedStable": True,
"label": "4/1",
- }
+ },
],
"speed": 30,
"label": "4",
@@ -622,7 +623,7 @@ DATA: list[dict[str, Any]] = [
"currentZones": 1,
"configuredZones": 0,
"systemStatus": "CoolingKo",
- }
+ },
],
"inputs": {"states": ["ok", "Not Inserted"]},
"expected": {"result": "failure", "messages": ["Fan 1/1 on Fan Tray 1 is: 'down'"]},
@@ -653,7 +654,7 @@ DATA: list[dict[str, Any]] = [
"speedHwOverride": True,
"speedStable": True,
"label": "PowerSupply1/1",
- }
+ },
],
"speed": 30,
"label": "PowerSupply1",
@@ -671,7 +672,7 @@ DATA: list[dict[str, Any]] = [
"speedHwOverride": True,
"speedStable": True,
"label": "PowerSupply2/1",
- }
+ },
],
"speed": 30,
"label": "PowerSupply2",
@@ -691,7 +692,7 @@ DATA: list[dict[str, Any]] = [
"speedHwOverride": False,
"speedStable": True,
"label": "1/1",
- }
+ },
],
"speed": 30,
"label": "1",
@@ -709,7 +710,7 @@ DATA: list[dict[str, Any]] = [
"speedHwOverride": False,
"speedStable": True,
"label": "2/1",
- }
+ },
],
"speed": 30,
"label": "2",
@@ -727,7 +728,7 @@ DATA: list[dict[str, Any]] = [
"speedHwOverride": False,
"speedStable": True,
"label": "3/1",
- }
+ },
],
"speed": 30,
"label": "3",
@@ -745,7 +746,7 @@ DATA: list[dict[str, Any]] = [
"speedHwOverride": False,
"speedStable": True,
"label": "4/1",
- }
+ },
],
"speed": 30,
"label": "4",
@@ -755,7 +756,7 @@ DATA: list[dict[str, Any]] = [
"currentZones": 1,
"configuredZones": 0,
"systemStatus": "CoolingKo",
- }
+ },
],
"inputs": {"states": ["ok", "Not Inserted"]},
"expected": {"result": "failure", "messages": ["Fan PowerSupply1/1 on PowerSupply PowerSupply1 is: 'down'"]},
@@ -801,8 +802,8 @@ DATA: list[dict[str, Any]] = [
"outputCurrent": 9.828125,
"managed": True,
},
- }
- }
+ },
+ },
],
"inputs": {"states": ["ok"]},
"expected": {"result": "success"},
@@ -848,8 +849,8 @@ DATA: list[dict[str, Any]] = [
"outputCurrent": 9.828125,
"managed": True,
},
- }
- }
+ },
+ },
],
"inputs": {"states": ["ok", "Not Inserted"]},
"expected": {"result": "success"},
@@ -895,8 +896,8 @@ DATA: list[dict[str, Any]] = [
"outputCurrent": 9.828125,
"managed": True,
},
- }
- }
+ },
+ },
],
"inputs": {"states": ["ok"]},
"expected": {"result": "failure", "messages": ["The following power supplies status are not in the accepted states list: {'1': {'state': 'powerLoss'}}"]},
diff --git a/tests/units/anta_tests/test_interfaces.py b/tests/units/anta_tests/test_interfaces.py
index 5b0d845..58f568f 100644
--- a/tests/units/anta_tests/test_interfaces.py
+++ b/tests/units/anta_tests/test_interfaces.py
@@ -1,7 +1,9 @@
# 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 inputs for anta.tests.hardware"""
+"""Test inputs for anta.tests.hardware."""
+
+# pylint: disable=C0302
from __future__ import annotations
from typing import Any
@@ -30,25 +32,772 @@ DATA: list[dict[str, Any]] = [
"name": "success",
"test": VerifyInterfaceUtilization,
"eos_data": [
- """Port Name Intvl In Mbps % In Kpps Out Mbps % Out Kpps
-Et1 5:00 0.0 0.0% 0 0.0 0.0% 0
-Et4 5:00 0.0 0.0% 0 0.0 0.0% 0
-"""
+ {
+ "interfaces": {
+ "Ethernet1/1": {
+ "description": "P2P_LINK_TO_DC1-SPINE1_Ethernet1/1",
+ "interval": 300,
+ "inBpsRate": 2242.2497205060313,
+ "inPktsRate": 0.00028663359326985426,
+ "inPpsRate": 3.9005388262031966,
+ "outBpsRate": 0.0,
+ "outPktsRate": 0.0,
+ "outPpsRate": 0.0,
+ "lastUpdateTimestamp": 1710253727.138605,
+ },
+ "Port-Channel31": {
+ "description": "MLAG_PEER_dc1-leaf1b_Po31",
+ "interval": 300,
+ "inBpsRate": 1862.4876594267096,
+ "inPktsRate": 0.00011473185873493155,
+ "inPpsRate": 2.7009344704495084,
+ "outBpsRate": 1758.0044570479704,
+ "outPktsRate": 0.00010844978034772172,
+ "outPpsRate": 2.5686946869154013,
+ "lastUpdateTimestamp": 1710253726.4029949,
+ },
+ }
+ },
+ {
+ "interfaces": {
+ "Ethernet1/1": {
+ "name": "Ethernet1/1",
+ "forwardingModel": "routed",
+ "lineProtocolStatus": "up",
+ "interfaceStatus": "connected",
+ "hardware": "ethernet",
+ "interfaceAddress": [
+ {
+ "primaryIp": {"address": "10.255.255.1", "maskLen": 31},
+ "secondaryIps": {},
+ "secondaryIpsOrderedList": [],
+ "virtualIp": {"address": "0.0.0.0", "maskLen": 0},
+ "virtualSecondaryIps": {},
+ "virtualSecondaryIpsOrderedList": [],
+ "broadcastAddress": "255.255.255.255",
+ "dhcp": False,
+ }
+ ],
+ "physicalAddress": "aa:c1:ab:7e:76:36",
+ "burnedInAddress": "aa:c1:ab:7e:76:36",
+ "description": "P2P_LINK_TO_DC1-SPINE1_Ethernet1/1",
+ "bandwidth": 1000000000,
+ "mtu": 1500,
+ "l3MtuConfigured": True,
+ "l2Mru": 0,
+ "lastStatusChangeTimestamp": 1710234511.3085763,
+ "interfaceStatistics": {
+ "updateInterval": 300.0,
+ "inBitsRate": 2240.0023281094,
+ "inPktsRate": 3.8978070399448654,
+ "outBitsRate": 0.0,
+ "outPktsRate": 0.0,
+ },
+ "interfaceCounters": {
+ "inOctets": 5413008,
+ "inUcastPkts": 74693,
+ "inMulticastPkts": 643,
+ "inBroadcastPkts": 1,
+ "inDiscards": 0,
+ "inTotalPkts": 75337,
+ "outOctets": 0,
+ "outUcastPkts": 0,
+ "outMulticastPkts": 0,
+ "outBroadcastPkts": 0,
+ "outDiscards": 0,
+ "outTotalPkts": 0,
+ "linkStatusChanges": 2,
+ "totalInErrors": 0,
+ "inputErrorsDetail": {"runtFrames": 0, "giantFrames": 0, "fcsErrors": 0, "alignmentErrors": 0, "symbolErrors": 0, "rxPause": 0},
+ "totalOutErrors": 0,
+ "outputErrorsDetail": {"collisions": 0, "lateCollisions": 0, "deferredTransmissions": 0, "txPause": 0},
+ "counterRefreshTime": 1710253760.6489396,
+ },
+ "duplex": "duplexFull",
+ "autoNegotiate": "unknown",
+ "loopbackMode": "loopbackNone",
+ "lanes": 0,
+ },
+ "Port-Channel31": {
+ "name": "Port-Channel31",
+ "forwardingModel": "bridged",
+ "lineProtocolStatus": "up",
+ "interfaceStatus": "connected",
+ "hardware": "portChannel",
+ "interfaceAddress": [],
+ "physicalAddress": "aa:c1:ab:72:58:40",
+ "description": "MLAG_PEER_dc1-leaf1b_Po31",
+ "bandwidth": 2000000000,
+ "mtu": 9214,
+ "l3MtuConfigured": False,
+ "l2Mru": 0,
+ "lastStatusChangeTimestamp": 1710234510.1133935,
+ "interfaceStatistics": {
+ "updateInterval": 300.0,
+ "inBitsRate": 1854.287898883752,
+ "inPktsRate": 2.6902775246495665,
+ "outBitsRate": 1749.1141130864632,
+ "outPktsRate": 2.5565618978302362,
+ },
+ "interfaceCounters": {
+ "inOctets": 4475556,
+ "inUcastPkts": 48949,
+ "inMulticastPkts": 2579,
+ "inBroadcastPkts": 2,
+ "inDiscards": 0,
+ "inTotalPkts": 51530,
+ "outOctets": 4230011,
+ "outUcastPkts": 48982,
+ "outMulticastPkts": 6,
+ "outBroadcastPkts": 2,
+ "outDiscards": 0,
+ "outTotalPkts": 48990,
+ "linkStatusChanges": 2,
+ "totalInErrors": 0,
+ "totalOutErrors": 0,
+ "counterRefreshTime": 1710253760.6500373,
+ },
+ "memberInterfaces": {
+ "Ethernet3/1": {"bandwidth": 1000000000, "duplex": "duplexFull"},
+ "Ethernet4/1": {"bandwidth": 1000000000, "duplex": "duplexFull"},
+ },
+ "fallbackEnabled": False,
+ "fallbackEnabledType": "fallbackNone",
+ },
+ }
+ },
],
- "inputs": None,
+ "inputs": {"threshold": 70.0},
+ "expected": {"result": "success"},
+ },
+ {
+ "name": "success-ignored-interface",
+ "test": VerifyInterfaceUtilization,
+ "eos_data": [
+ {
+ "interfaces": {
+ "Ethernet1/1": {
+ "description": "P2P_LINK_TO_DC1-SPINE1_Ethernet1/1",
+ "interval": 300,
+ "inBpsRate": 2242.2497205060313,
+ "inPktsRate": 0.00028663359326985426,
+ "inPpsRate": 3.9005388262031966,
+ "outBpsRate": 0.0,
+ "outPktsRate": 0.0,
+ "outPpsRate": 0.0,
+ "lastUpdateTimestamp": 1710253727.138605,
+ },
+ "Port-Channel31": {
+ "description": "MLAG_PEER_dc1-leaf1b_Po31",
+ "interval": 300,
+ "inBpsRate": 1862.4876594267096,
+ "inPktsRate": 0.00011473185873493155,
+ "inPpsRate": 2.7009344704495084,
+ "outBpsRate": 1758.0044570479704,
+ "outPktsRate": 0.00010844978034772172,
+ "outPpsRate": 2.5686946869154013,
+ "lastUpdateTimestamp": 1710253726.4029949,
+ },
+ "Port-Channel51": {
+ "description": "dc1-leaf1-server1",
+ "interval": 300,
+ "inBpsRate": 0.0023680437493116147,
+ "inPpsRate": 2.3125427239371238e-06,
+ "outBpsRate": 0.0,
+ "outPpsRate": 0.0,
+ "lastUpdateTimestamp": 1712928643.7805147,
+ },
+ },
+ },
+ {
+ "interfaces": {
+ "Ethernet1/1": {
+ "name": "Ethernet1/1",
+ "forwardingModel": "routed",
+ "lineProtocolStatus": "up",
+ "interfaceStatus": "connected",
+ "hardware": "ethernet",
+ "interfaceAddress": [
+ {
+ "primaryIp": {"address": "10.255.255.1", "maskLen": 31},
+ "secondaryIps": {},
+ "secondaryIpsOrderedList": [],
+ "virtualIp": {"address": "0.0.0.0", "maskLen": 0},
+ "virtualSecondaryIps": {},
+ "virtualSecondaryIpsOrderedList": [],
+ "broadcastAddress": "255.255.255.255",
+ "dhcp": False,
+ }
+ ],
+ "physicalAddress": "aa:c1:ab:7e:76:36",
+ "burnedInAddress": "aa:c1:ab:7e:76:36",
+ "description": "P2P_LINK_TO_DC1-SPINE1_Ethernet1/1",
+ "bandwidth": 1000000000,
+ "mtu": 1500,
+ "l3MtuConfigured": True,
+ "l2Mru": 0,
+ "lastStatusChangeTimestamp": 1710234511.3085763,
+ "interfaceStatistics": {
+ "updateInterval": 300.0,
+ "inBitsRate": 2240.0023281094,
+ "inPktsRate": 3.8978070399448654,
+ "outBitsRate": 0.0,
+ "outPktsRate": 0.0,
+ },
+ "interfaceCounters": {
+ "inOctets": 5413008,
+ "inUcastPkts": 74693,
+ "inMulticastPkts": 643,
+ "inBroadcastPkts": 1,
+ "inDiscards": 0,
+ "inTotalPkts": 75337,
+ "outOctets": 0,
+ "outUcastPkts": 0,
+ "outMulticastPkts": 0,
+ "outBroadcastPkts": 0,
+ "outDiscards": 0,
+ "outTotalPkts": 0,
+ "linkStatusChanges": 2,
+ "totalInErrors": 0,
+ "inputErrorsDetail": {"runtFrames": 0, "giantFrames": 0, "fcsErrors": 0, "alignmentErrors": 0, "symbolErrors": 0, "rxPause": 0},
+ "totalOutErrors": 0,
+ "outputErrorsDetail": {"collisions": 0, "lateCollisions": 0, "deferredTransmissions": 0, "txPause": 0},
+ "counterRefreshTime": 1710253760.6489396,
+ },
+ "duplex": "duplexFull",
+ "autoNegotiate": "unknown",
+ "loopbackMode": "loopbackNone",
+ "lanes": 0,
+ },
+ "Port-Channel31": {
+ "name": "Port-Channel31",
+ "forwardingModel": "bridged",
+ "lineProtocolStatus": "up",
+ "interfaceStatus": "connected",
+ "hardware": "portChannel",
+ "interfaceAddress": [],
+ "physicalAddress": "aa:c1:ab:72:58:40",
+ "description": "MLAG_PEER_dc1-leaf1b_Po31",
+ "bandwidth": 2000000000,
+ "mtu": 9214,
+ "l3MtuConfigured": False,
+ "l2Mru": 0,
+ "lastStatusChangeTimestamp": 1710234510.1133935,
+ "interfaceStatistics": {
+ "updateInterval": 300.0,
+ "inBitsRate": 1854.287898883752,
+ "inPktsRate": 2.6902775246495665,
+ "outBitsRate": 1749.1141130864632,
+ "outPktsRate": 2.5565618978302362,
+ },
+ "interfaceCounters": {
+ "inOctets": 4475556,
+ "inUcastPkts": 48949,
+ "inMulticastPkts": 2579,
+ "inBroadcastPkts": 2,
+ "inDiscards": 0,
+ "inTotalPkts": 51530,
+ "outOctets": 4230011,
+ "outUcastPkts": 48982,
+ "outMulticastPkts": 6,
+ "outBroadcastPkts": 2,
+ "outDiscards": 0,
+ "outTotalPkts": 48990,
+ "linkStatusChanges": 2,
+ "totalInErrors": 0,
+ "totalOutErrors": 0,
+ "counterRefreshTime": 1710253760.6500373,
+ },
+ "memberInterfaces": {
+ "Ethernet3/1": {"bandwidth": 1000000000, "duplex": "duplexFull"},
+ "Ethernet4/1": {"bandwidth": 1000000000, "duplex": "duplexFull"},
+ },
+ "fallbackEnabled": False,
+ "fallbackEnabledType": "fallbackNone",
+ },
+ "Port-Channel51": {
+ "name": "Port-Channel51",
+ "forwardingModel": "bridged",
+ "lineProtocolStatus": "lowerLayerDown",
+ "interfaceStatus": "notconnect",
+ "hardware": "portChannel",
+ "interfaceAddress": [],
+ "physicalAddress": "00:00:00:00:00:00",
+ "description": "dc1-leaf1-server1",
+ "bandwidth": 0,
+ "mtu": 9214,
+ "l3MtuConfigured": False,
+ "l2Mru": 0,
+ "lastStatusChangeTimestamp": 1712925798.5035574,
+ "interfaceStatistics": {
+ "updateInterval": 300.0,
+ "inBitsRate": 0.00839301770723288,
+ "inPktsRate": 8.19630635471961e-06,
+ "outBitsRate": 0.0,
+ "outPktsRate": 0.0,
+ },
+ "interfaceCounters": {
+ "inOctets": 329344,
+ "inUcastPkts": 0,
+ "inMulticastPkts": 2573,
+ "inBroadcastPkts": 0,
+ "inDiscards": 0,
+ "inTotalPkts": 2573,
+ "outOctets": 0,
+ "outUcastPkts": 0,
+ "outMulticastPkts": 0,
+ "outBroadcastPkts": 0,
+ "outDiscards": 0,
+ "outTotalPkts": 0,
+ "linkStatusChanges": 3,
+ "totalInErrors": 0,
+ "totalOutErrors": 0,
+ "counterRefreshTime": 1712928265.9816775,
+ },
+ "memberInterfaces": {},
+ "fallbackEnabled": False,
+ "fallbackEnabledType": "fallbackNone",
+ },
+ }
+ },
+ ],
+ "inputs": {"threshold": 70.0},
"expected": {"result": "success"},
},
{
"name": "failure",
"test": VerifyInterfaceUtilization,
"eos_data": [
- """Port Name Intvl In Mbps % In Kpps Out Mbps % Out Kpps
-Et1 5:00 0.0 0.0% 0 0.0 80.0% 0
-Et4 5:00 0.0 99.9% 0 0.0 0.0% 0
-"""
+ {
+ "interfaces": {
+ "Ethernet1/1": {
+ "description": "P2P_LINK_TO_DC1-SPINE1_Ethernet1/1",
+ "interval": 300,
+ "inBpsRate": 100000000.0,
+ "inPktsRate": 0.00028663359326985426,
+ "inPpsRate": 3.9005388262031966,
+ "outBpsRate": 0.0,
+ "outPktsRate": 0.0,
+ "outPpsRate": 0.0,
+ "lastUpdateTimestamp": 1710253727.138605,
+ },
+ "Port-Channel31": {
+ "description": "MLAG_PEER_dc1-leaf1b_Po31",
+ "interval": 300,
+ "inBpsRate": 1862.4876594267096,
+ "inPktsRate": 0.00011473185873493155,
+ "inPpsRate": 2.7009344704495084,
+ "outBpsRate": 100000000.0,
+ "outPktsRate": 0.00010844978034772172,
+ "outPpsRate": 2.5686946869154013,
+ "lastUpdateTimestamp": 1710253726.4029949,
+ },
+ }
+ },
+ {
+ "interfaces": {
+ "Ethernet1/1": {
+ "name": "Ethernet1/1",
+ "forwardingModel": "routed",
+ "lineProtocolStatus": "up",
+ "interfaceStatus": "connected",
+ "hardware": "ethernet",
+ "interfaceAddress": [
+ {
+ "primaryIp": {"address": "10.255.255.1", "maskLen": 31},
+ "secondaryIps": {},
+ "secondaryIpsOrderedList": [],
+ "virtualIp": {"address": "0.0.0.0", "maskLen": 0},
+ "virtualSecondaryIps": {},
+ "virtualSecondaryIpsOrderedList": [],
+ "broadcastAddress": "255.255.255.255",
+ "dhcp": False,
+ }
+ ],
+ "physicalAddress": "aa:c1:ab:7e:76:36",
+ "burnedInAddress": "aa:c1:ab:7e:76:36",
+ "description": "P2P_LINK_TO_DC1-SPINE1_Ethernet1/1",
+ "bandwidth": 1000000000,
+ "mtu": 1500,
+ "l3MtuConfigured": True,
+ "l2Mru": 0,
+ "lastStatusChangeTimestamp": 1710234511.3085763,
+ "interfaceStatistics": {
+ "updateInterval": 300.0,
+ "inBitsRate": 2240.0023281094,
+ "inPktsRate": 3.8978070399448654,
+ "outBitsRate": 0.0,
+ "outPktsRate": 0.0,
+ },
+ "interfaceCounters": {
+ "inOctets": 5413008,
+ "inUcastPkts": 74693,
+ "inMulticastPkts": 643,
+ "inBroadcastPkts": 1,
+ "inDiscards": 0,
+ "inTotalPkts": 75337,
+ "outOctets": 0,
+ "outUcastPkts": 0,
+ "outMulticastPkts": 0,
+ "outBroadcastPkts": 0,
+ "outDiscards": 0,
+ "outTotalPkts": 0,
+ "linkStatusChanges": 2,
+ "totalInErrors": 0,
+ "inputErrorsDetail": {"runtFrames": 0, "giantFrames": 0, "fcsErrors": 0, "alignmentErrors": 0, "symbolErrors": 0, "rxPause": 0},
+ "totalOutErrors": 0,
+ "outputErrorsDetail": {"collisions": 0, "lateCollisions": 0, "deferredTransmissions": 0, "txPause": 0},
+ "counterRefreshTime": 1710253760.6489396,
+ },
+ "duplex": "duplexFull",
+ "autoNegotiate": "unknown",
+ "loopbackMode": "loopbackNone",
+ "lanes": 0,
+ },
+ "Port-Channel31": {
+ "name": "Port-Channel31",
+ "forwardingModel": "bridged",
+ "lineProtocolStatus": "up",
+ "interfaceStatus": "connected",
+ "hardware": "portChannel",
+ "interfaceAddress": [],
+ "physicalAddress": "aa:c1:ab:72:58:40",
+ "description": "MLAG_PEER_dc1-leaf1b_Po31",
+ "bandwidth": 2000000000,
+ "mtu": 9214,
+ "l3MtuConfigured": False,
+ "l2Mru": 0,
+ "lastStatusChangeTimestamp": 1710234510.1133935,
+ "interfaceStatistics": {
+ "updateInterval": 300.0,
+ "inBitsRate": 1854.287898883752,
+ "inPktsRate": 2.6902775246495665,
+ "outBitsRate": 1749.1141130864632,
+ "outPktsRate": 2.5565618978302362,
+ },
+ "interfaceCounters": {
+ "inOctets": 4475556,
+ "inUcastPkts": 48949,
+ "inMulticastPkts": 2579,
+ "inBroadcastPkts": 2,
+ "inDiscards": 0,
+ "inTotalPkts": 51530,
+ "outOctets": 4230011,
+ "outUcastPkts": 48982,
+ "outMulticastPkts": 6,
+ "outBroadcastPkts": 2,
+ "outDiscards": 0,
+ "outTotalPkts": 48990,
+ "linkStatusChanges": 2,
+ "totalInErrors": 0,
+ "totalOutErrors": 0,
+ "counterRefreshTime": 1710253760.6500373,
+ },
+ "memberInterfaces": {
+ "Ethernet3/1": {"bandwidth": 1000000000, "duplex": "duplexFull"},
+ "Ethernet4/1": {"bandwidth": 1000000000, "duplex": "duplexFull"},
+ },
+ "fallbackEnabled": False,
+ "fallbackEnabledType": "fallbackNone",
+ },
+ }
+ },
],
- "inputs": None,
- "expected": {"result": "failure", "messages": ["The following interfaces have a usage > 75%: {'Et1': '80.0%', 'Et4': '99.9%'}"]},
+ "inputs": {"threshold": 3.0},
+ "expected": {
+ "result": "failure",
+ "messages": ["The following interfaces have a usage > 3.0%: {'Ethernet1/1': {'inBpsRate': 10.0}, 'Port-Channel31': {'outBpsRate': 5.0}}"],
+ },
+ },
+ {
+ "name": "error-duplex-half",
+ "test": VerifyInterfaceUtilization,
+ "eos_data": [
+ {
+ "interfaces": {
+ "Ethernet1/1": {
+ "description": "P2P_LINK_TO_DC1-SPINE1_Ethernet1/1",
+ "interval": 300,
+ "inBpsRate": 2242.2497205060313,
+ "inPktsRate": 0.00028663359326985426,
+ "inPpsRate": 3.9005388262031966,
+ "outBpsRate": 0.0,
+ "outPktsRate": 0.0,
+ "outPpsRate": 0.0,
+ "lastUpdateTimestamp": 1710253727.138605,
+ },
+ "Port-Channel31": {
+ "description": "MLAG_PEER_dc1-leaf1b_Po31",
+ "interval": 300,
+ "inBpsRate": 1862.4876594267096,
+ "inPktsRate": 0.00011473185873493155,
+ "inPpsRate": 2.7009344704495084,
+ "outBpsRate": 1758.0044570479704,
+ "outPktsRate": 0.00010844978034772172,
+ "outPpsRate": 2.5686946869154013,
+ "lastUpdateTimestamp": 1710253726.4029949,
+ },
+ }
+ },
+ {
+ "interfaces": {
+ "Ethernet1/1": {
+ "name": "Ethernet1/1",
+ "forwardingModel": "routed",
+ "lineProtocolStatus": "up",
+ "interfaceStatus": "connected",
+ "hardware": "ethernet",
+ "interfaceAddress": [
+ {
+ "primaryIp": {"address": "10.255.255.1", "maskLen": 31},
+ "secondaryIps": {},
+ "secondaryIpsOrderedList": [],
+ "virtualIp": {"address": "0.0.0.0", "maskLen": 0},
+ "virtualSecondaryIps": {},
+ "virtualSecondaryIpsOrderedList": [],
+ "broadcastAddress": "255.255.255.255",
+ "dhcp": False,
+ }
+ ],
+ "physicalAddress": "aa:c1:ab:7e:76:36",
+ "burnedInAddress": "aa:c1:ab:7e:76:36",
+ "description": "P2P_LINK_TO_DC1-SPINE1_Ethernet1/1",
+ "bandwidth": 1000000000,
+ "mtu": 1500,
+ "l3MtuConfigured": True,
+ "l2Mru": 0,
+ "lastStatusChangeTimestamp": 1710234511.3085763,
+ "interfaceStatistics": {
+ "updateInterval": 300.0,
+ "inBitsRate": 2240.0023281094,
+ "inPktsRate": 3.8978070399448654,
+ "outBitsRate": 0.0,
+ "outPktsRate": 0.0,
+ },
+ "interfaceCounters": {
+ "inOctets": 5413008,
+ "inUcastPkts": 74693,
+ "inMulticastPkts": 643,
+ "inBroadcastPkts": 1,
+ "inDiscards": 0,
+ "inTotalPkts": 75337,
+ "outOctets": 0,
+ "outUcastPkts": 0,
+ "outMulticastPkts": 0,
+ "outBroadcastPkts": 0,
+ "outDiscards": 0,
+ "outTotalPkts": 0,
+ "linkStatusChanges": 2,
+ "totalInErrors": 0,
+ "inputErrorsDetail": {"runtFrames": 0, "giantFrames": 0, "fcsErrors": 0, "alignmentErrors": 0, "symbolErrors": 0, "rxPause": 0},
+ "totalOutErrors": 0,
+ "outputErrorsDetail": {"collisions": 0, "lateCollisions": 0, "deferredTransmissions": 0, "txPause": 0},
+ "counterRefreshTime": 1710253760.6489396,
+ },
+ "duplex": "duplexHalf",
+ "autoNegotiate": "unknown",
+ "loopbackMode": "loopbackNone",
+ "lanes": 0,
+ },
+ "Port-Channel31": {
+ "name": "Port-Channel31",
+ "forwardingModel": "bridged",
+ "lineProtocolStatus": "up",
+ "interfaceStatus": "connected",
+ "hardware": "portChannel",
+ "interfaceAddress": [],
+ "physicalAddress": "aa:c1:ab:72:58:40",
+ "description": "MLAG_PEER_dc1-leaf1b_Po31",
+ "bandwidth": 2000000000,
+ "mtu": 9214,
+ "l3MtuConfigured": False,
+ "l2Mru": 0,
+ "lastStatusChangeTimestamp": 1710234510.1133935,
+ "interfaceStatistics": {
+ "updateInterval": 300.0,
+ "inBitsRate": 1854.287898883752,
+ "inPktsRate": 2.6902775246495665,
+ "outBitsRate": 1749.1141130864632,
+ "outPktsRate": 2.5565618978302362,
+ },
+ "interfaceCounters": {
+ "inOctets": 4475556,
+ "inUcastPkts": 48949,
+ "inMulticastPkts": 2579,
+ "inBroadcastPkts": 2,
+ "inDiscards": 0,
+ "inTotalPkts": 51530,
+ "outOctets": 4230011,
+ "outUcastPkts": 48982,
+ "outMulticastPkts": 6,
+ "outBroadcastPkts": 2,
+ "outDiscards": 0,
+ "outTotalPkts": 48990,
+ "linkStatusChanges": 2,
+ "totalInErrors": 0,
+ "totalOutErrors": 0,
+ "counterRefreshTime": 1710253760.6500373,
+ },
+ "memberInterfaces": {
+ "Ethernet3/1": {"bandwidth": 1000000000, "duplex": "duplexFull"},
+ "Ethernet4/1": {"bandwidth": 1000000000, "duplex": "duplexFull"},
+ },
+ "fallbackEnabled": False,
+ "fallbackEnabledType": "fallbackNone",
+ },
+ }
+ },
+ ],
+ "inputs": {"threshold": 70.0},
+ "expected": {
+ "result": "error",
+ "messages": ["Interface Ethernet1/1 or one of its member interfaces is not Full-Duplex. VerifyInterfaceUtilization has not been implemented."],
+ },
+ },
+ {
+ "name": "error-duplex-half-po",
+ "test": VerifyInterfaceUtilization,
+ "eos_data": [
+ {
+ "interfaces": {
+ "Ethernet1/1": {
+ "description": "P2P_LINK_TO_DC1-SPINE1_Ethernet1/1",
+ "interval": 300,
+ "inBpsRate": 2242.2497205060313,
+ "inPktsRate": 0.00028663359326985426,
+ "inPpsRate": 3.9005388262031966,
+ "outBpsRate": 0.0,
+ "outPktsRate": 0.0,
+ "outPpsRate": 0.0,
+ "lastUpdateTimestamp": 1710253727.138605,
+ },
+ "Port-Channel31": {
+ "description": "MLAG_PEER_dc1-leaf1b_Po31",
+ "interval": 300,
+ "inBpsRate": 1862.4876594267096,
+ "inPktsRate": 0.00011473185873493155,
+ "inPpsRate": 2.7009344704495084,
+ "outBpsRate": 1758.0044570479704,
+ "outPktsRate": 0.00010844978034772172,
+ "outPpsRate": 2.5686946869154013,
+ "lastUpdateTimestamp": 1710253726.4029949,
+ },
+ }
+ },
+ {
+ "interfaces": {
+ "Ethernet1/1": {
+ "name": "Ethernet1/1",
+ "forwardingModel": "routed",
+ "lineProtocolStatus": "up",
+ "interfaceStatus": "connected",
+ "hardware": "ethernet",
+ "interfaceAddress": [
+ {
+ "primaryIp": {"address": "10.255.255.1", "maskLen": 31},
+ "secondaryIps": {},
+ "secondaryIpsOrderedList": [],
+ "virtualIp": {"address": "0.0.0.0", "maskLen": 0},
+ "virtualSecondaryIps": {},
+ "virtualSecondaryIpsOrderedList": [],
+ "broadcastAddress": "255.255.255.255",
+ "dhcp": False,
+ }
+ ],
+ "physicalAddress": "aa:c1:ab:7e:76:36",
+ "burnedInAddress": "aa:c1:ab:7e:76:36",
+ "description": "P2P_LINK_TO_DC1-SPINE1_Ethernet1/1",
+ "bandwidth": 1000000000,
+ "mtu": 1500,
+ "l3MtuConfigured": True,
+ "l2Mru": 0,
+ "lastStatusChangeTimestamp": 1710234511.3085763,
+ "interfaceStatistics": {
+ "updateInterval": 300.0,
+ "inBitsRate": 2240.0023281094,
+ "inPktsRate": 3.8978070399448654,
+ "outBitsRate": 0.0,
+ "outPktsRate": 0.0,
+ },
+ "interfaceCounters": {
+ "inOctets": 5413008,
+ "inUcastPkts": 74693,
+ "inMulticastPkts": 643,
+ "inBroadcastPkts": 1,
+ "inDiscards": 0,
+ "inTotalPkts": 75337,
+ "outOctets": 0,
+ "outUcastPkts": 0,
+ "outMulticastPkts": 0,
+ "outBroadcastPkts": 0,
+ "outDiscards": 0,
+ "outTotalPkts": 0,
+ "linkStatusChanges": 2,
+ "totalInErrors": 0,
+ "inputErrorsDetail": {"runtFrames": 0, "giantFrames": 0, "fcsErrors": 0, "alignmentErrors": 0, "symbolErrors": 0, "rxPause": 0},
+ "totalOutErrors": 0,
+ "outputErrorsDetail": {"collisions": 0, "lateCollisions": 0, "deferredTransmissions": 0, "txPause": 0},
+ "counterRefreshTime": 1710253760.6489396,
+ },
+ "duplex": "duplexFull",
+ "autoNegotiate": "unknown",
+ "loopbackMode": "loopbackNone",
+ "lanes": 0,
+ },
+ "Port-Channel31": {
+ "name": "Port-Channel31",
+ "forwardingModel": "bridged",
+ "lineProtocolStatus": "up",
+ "interfaceStatus": "connected",
+ "hardware": "portChannel",
+ "interfaceAddress": [],
+ "physicalAddress": "aa:c1:ab:72:58:40",
+ "description": "MLAG_PEER_dc1-leaf1b_Po31",
+ "bandwidth": 2000000000,
+ "mtu": 9214,
+ "l3MtuConfigured": False,
+ "l2Mru": 0,
+ "lastStatusChangeTimestamp": 1710234510.1133935,
+ "interfaceStatistics": {
+ "updateInterval": 300.0,
+ "inBitsRate": 1854.287898883752,
+ "inPktsRate": 2.6902775246495665,
+ "outBitsRate": 1749.1141130864632,
+ "outPktsRate": 2.5565618978302362,
+ },
+ "interfaceCounters": {
+ "inOctets": 4475556,
+ "inUcastPkts": 48949,
+ "inMulticastPkts": 2579,
+ "inBroadcastPkts": 2,
+ "inDiscards": 0,
+ "inTotalPkts": 51530,
+ "outOctets": 4230011,
+ "outUcastPkts": 48982,
+ "outMulticastPkts": 6,
+ "outBroadcastPkts": 2,
+ "outDiscards": 0,
+ "outTotalPkts": 48990,
+ "linkStatusChanges": 2,
+ "totalInErrors": 0,
+ "totalOutErrors": 0,
+ "counterRefreshTime": 1710253760.6500373,
+ },
+ "memberInterfaces": {
+ "Ethernet3/1": {"bandwidth": 1000000000, "duplex": "duplexHalf"},
+ "Ethernet4/1": {"bandwidth": 1000000000, "duplex": "duplexFull"},
+ },
+ "fallbackEnabled": False,
+ "fallbackEnabledType": "fallbackNone",
+ },
+ }
+ },
+ ],
+ "inputs": {"threshold": 70.0},
+ "expected": {
+ "result": "error",
+ "messages": ["Interface Port-Channel31 or one of its member interfaces is not Full-Duplex. VerifyInterfaceUtilization has not been implemented."],
+ },
},
{
"name": "success",
@@ -58,8 +807,8 @@ Et4 5:00 0.0 99.9% 0 0.0 0.0% 0
"interfaceErrorCounters": {
"Ethernet1": {"inErrors": 0, "frameTooLongs": 0, "outErrors": 0, "frameTooShorts": 0, "fcsErrors": 0, "alignmentErrors": 0, "symbolErrors": 0},
"Ethernet6": {"inErrors": 0, "frameTooLongs": 0, "outErrors": 0, "frameTooShorts": 0, "fcsErrors": 0, "alignmentErrors": 0, "symbolErrors": 0},
- }
- }
+ },
+ },
],
"inputs": None,
"expected": {"result": "success"},
@@ -72,8 +821,8 @@ Et4 5:00 0.0 99.9% 0 0.0 0.0% 0
"interfaceErrorCounters": {
"Ethernet1": {"inErrors": 42, "frameTooLongs": 0, "outErrors": 0, "frameTooShorts": 0, "fcsErrors": 0, "alignmentErrors": 0, "symbolErrors": 0},
"Ethernet6": {"inErrors": 0, "frameTooLongs": 0, "outErrors": 0, "frameTooShorts": 0, "fcsErrors": 0, "alignmentErrors": 666, "symbolErrors": 0},
- }
- }
+ },
+ },
],
"inputs": None,
"expected": {
@@ -81,7 +830,7 @@ Et4 5:00 0.0 99.9% 0 0.0 0.0% 0
"messages": [
"The following interface(s) have non-zero error counters: [{'Ethernet1': {'inErrors': 42, 'frameTooLongs': 0, 'outErrors': 0, 'frameTooShorts': 0,"
" 'fcsErrors': 0, 'alignmentErrors': 0, 'symbolErrors': 0}}, {'Ethernet6': {'inErrors': 0, 'frameTooLongs': 0, 'outErrors': 0, 'frameTooShorts':"
- " 0, 'fcsErrors': 0, 'alignmentErrors': 666, 'symbolErrors': 0}}]"
+ " 0, 'fcsErrors': 0, 'alignmentErrors': 666, 'symbolErrors': 0}}]",
],
},
},
@@ -93,8 +842,8 @@ Et4 5:00 0.0 99.9% 0 0.0 0.0% 0
"interfaceErrorCounters": {
"Ethernet1": {"inErrors": 42, "frameTooLongs": 0, "outErrors": 10, "frameTooShorts": 0, "fcsErrors": 0, "alignmentErrors": 0, "symbolErrors": 0},
"Ethernet6": {"inErrors": 0, "frameTooLongs": 0, "outErrors": 0, "frameTooShorts": 0, "fcsErrors": 0, "alignmentErrors": 6, "symbolErrors": 10},
- }
- }
+ },
+ },
],
"inputs": None,
"expected": {
@@ -102,7 +851,7 @@ Et4 5:00 0.0 99.9% 0 0.0 0.0% 0
"messages": [
"The following interface(s) have non-zero error counters: [{'Ethernet1': {'inErrors': 42, 'frameTooLongs': 0, 'outErrors': 10, 'frameTooShorts': 0,"
" 'fcsErrors': 0, 'alignmentErrors': 0, 'symbolErrors': 0}}, {'Ethernet6': {'inErrors': 0, 'frameTooLongs': 0, 'outErrors': 0, 'frameTooShorts':"
- " 0, 'fcsErrors': 0, 'alignmentErrors': 6, 'symbolErrors': 10}}]"
+ " 0, 'fcsErrors': 0, 'alignmentErrors': 6, 'symbolErrors': 10}}]",
],
},
},
@@ -113,15 +862,15 @@ Et4 5:00 0.0 99.9% 0 0.0 0.0% 0
{
"interfaceErrorCounters": {
"Ethernet1": {"inErrors": 42, "frameTooLongs": 0, "outErrors": 2, "frameTooShorts": 0, "fcsErrors": 0, "alignmentErrors": 0, "symbolErrors": 0},
- }
- }
+ },
+ },
],
"inputs": None,
"expected": {
"result": "failure",
"messages": [
"The following interface(s) have non-zero error counters: [{'Ethernet1': {'inErrors': 42, 'frameTooLongs': 0, 'outErrors': 2, 'frameTooShorts': 0,"
- " 'fcsErrors': 0, 'alignmentErrors': 0, 'symbolErrors': 0}}]"
+ " 'fcsErrors': 0, 'alignmentErrors': 0, 'symbolErrors': 0}}]",
],
},
},
@@ -136,7 +885,7 @@ Et4 5:00 0.0 99.9% 0 0.0 0.0% 0
"Ethernet1": {"outDiscards": 0, "inDiscards": 0},
},
"outDiscardsTotal": 0,
- }
+ },
],
"inputs": None,
"expected": {"result": "success"},
@@ -152,14 +901,14 @@ Et4 5:00 0.0 99.9% 0 0.0 0.0% 0
"Ethernet1": {"outDiscards": 0, "inDiscards": 42},
},
"outDiscardsTotal": 0,
- }
+ },
],
"inputs": None,
"expected": {
"result": "failure",
"messages": [
"The following interfaces have non 0 discard counter(s): [{'Ethernet2': {'outDiscards': 42, 'inDiscards': 0}},"
- " {'Ethernet1': {'outDiscards': 0, 'inDiscards': 42}}]"
+ " {'Ethernet1': {'outDiscards': 0, 'inDiscards': 42}}]",
],
},
},
@@ -175,8 +924,8 @@ Et4 5:00 0.0 99.9% 0 0.0 0.0% 0
"Ethernet8": {
"linkStatus": "connected",
},
- }
- }
+ },
+ },
],
"inputs": None,
"expected": {"result": "success"},
@@ -193,8 +942,8 @@ Et4 5:00 0.0 99.9% 0 0.0 0.0% 0
"Ethernet8": {
"linkStatus": "errdisabled",
},
- }
- }
+ },
+ },
],
"inputs": None,
"expected": {"result": "failure", "messages": ["The following interfaces are in error disabled state: ['Management1', 'Ethernet8']"]},
@@ -208,8 +957,8 @@ Et4 5:00 0.0 99.9% 0 0.0 0.0% 0
"Ethernet8": {"interfaceStatus": "up", "description": "", "lineProtocolStatus": "up"},
"Ethernet2": {"interfaceStatus": "adminDown", "description": "", "lineProtocolStatus": "down"},
"Ethernet3": {"interfaceStatus": "up", "description": "", "lineProtocolStatus": "up"},
- }
- }
+ },
+ },
],
"inputs": {"interfaces": [{"name": "Ethernet2", "status": "adminDown"}, {"name": "Ethernet8", "status": "up"}, {"name": "Ethernet3", "status": "up"}]},
"expected": {"result": "success"},
@@ -257,8 +1006,8 @@ Et4 5:00 0.0 99.9% 0 0.0 0.0% 0
"Ethernet8": {"interfaceStatus": "up", "description": "", "lineProtocolStatus": "up"},
"Ethernet2": {"interfaceStatus": "adminDown", "description": "", "lineProtocolStatus": "down"},
"Ethernet3": {"interfaceStatus": "up", "description": "", "lineProtocolStatus": "up"},
- }
- }
+ },
+ },
],
"inputs": {"interfaces": [{"name": "ethernet2", "status": "adminDown"}, {"name": "ethernet8", "status": "up"}, {"name": "ethernet3", "status": "up"}]},
"expected": {"result": "success"},
@@ -272,8 +1021,8 @@ Et4 5:00 0.0 99.9% 0 0.0 0.0% 0
"Ethernet8": {"interfaceStatus": "up", "description": "", "lineProtocolStatus": "up"},
"Ethernet2": {"interfaceStatus": "adminDown", "description": "", "lineProtocolStatus": "down"},
"Ethernet3": {"interfaceStatus": "up", "description": "", "lineProtocolStatus": "up"},
- }
- }
+ },
+ },
],
"inputs": {"interfaces": [{"name": "eth2", "status": "adminDown"}, {"name": "et8", "status": "up"}, {"name": "et3", "status": "up"}]},
"expected": {"result": "success"},
@@ -285,8 +1034,8 @@ Et4 5:00 0.0 99.9% 0 0.0 0.0% 0
{
"interfaceDescriptions": {
"Port-Channel100": {"interfaceStatus": "up", "description": "", "lineProtocolStatus": "up"},
- }
- }
+ },
+ },
],
"inputs": {"interfaces": [{"name": "po100", "status": "up"}]},
"expected": {"result": "success"},
@@ -298,8 +1047,8 @@ Et4 5:00 0.0 99.9% 0 0.0 0.0% 0
{
"interfaceDescriptions": {
"Ethernet52/1.1963": {"interfaceStatus": "up", "description": "", "lineProtocolStatus": "up"},
- }
- }
+ },
+ },
],
"inputs": {"interfaces": [{"name": "Ethernet52/1.1963", "status": "up"}]},
"expected": {"result": "success"},
@@ -351,8 +1100,8 @@ Et4 5:00 0.0 99.9% 0 0.0 0.0% 0
"interfaceDescriptions": {
"Ethernet2": {"interfaceStatus": "up", "description": "", "lineProtocolStatus": "up"},
"Ethernet3": {"interfaceStatus": "up", "description": "", "lineProtocolStatus": "up"},
- }
- }
+ },
+ },
],
"inputs": {"interfaces": [{"name": "Ethernet2", "status": "up"}, {"name": "Ethernet8", "status": "up"}, {"name": "Ethernet3", "status": "up"}]},
"expected": {
@@ -369,8 +1118,8 @@ Et4 5:00 0.0 99.9% 0 0.0 0.0% 0
"Ethernet8": {"interfaceStatus": "down", "description": "", "lineProtocolStatus": "down"},
"Ethernet2": {"interfaceStatus": "up", "description": "", "lineProtocolStatus": "up"},
"Ethernet3": {"interfaceStatus": "up", "description": "", "lineProtocolStatus": "up"},
- }
- }
+ },
+ },
],
"inputs": {"interfaces": [{"name": "Ethernet2", "status": "up"}, {"name": "Ethernet8", "status": "up"}, {"name": "Ethernet3", "status": "up"}]},
"expected": {
@@ -387,8 +1136,8 @@ Et4 5:00 0.0 99.9% 0 0.0 0.0% 0
"Ethernet8": {"interfaceStatus": "up", "description": "", "lineProtocolStatus": "down"},
"Ethernet2": {"interfaceStatus": "up", "description": "", "lineProtocolStatus": "up"},
"Ethernet3": {"interfaceStatus": "up", "description": "", "lineProtocolStatus": "up"},
- }
- }
+ },
+ },
],
"inputs": {
"interfaces": [
@@ -454,9 +1203,9 @@ Et4 5:00 0.0 99.9% 0 0.0 0.0% 0
"active": True,
"reason": "",
"errdisabled": False,
- }
+ },
},
- }
+ },
],
"inputs": None,
"expected": {"result": "success"},
@@ -473,9 +1222,9 @@ Et4 5:00 0.0 99.9% 0 0.0 0.0% 0
"active": True,
"reason": "",
"errdisabled": False,
- }
+ },
},
- }
+ },
],
"inputs": None,
"expected": {"result": "failure", "messages": ["The following interfaces have none 0 storm-control drop counters {'Ethernet1': {'broadcast': 666}}"]},
@@ -496,9 +1245,9 @@ Et4 5:00 0.0 99.9% 0 0.0 0.0% 0
"inactivePorts": {},
"activePorts": {},
"inactiveLag": False,
- }
- }
- }
+ },
+ },
+ },
],
"inputs": None,
"expected": {"result": "success"},
@@ -519,9 +1268,9 @@ Et4 5:00 0.0 99.9% 0 0.0 0.0% 0
"inactivePorts": {"Ethernet8": {"reasonUnconfigured": "waiting for LACP response"}},
"activePorts": {},
"inactiveLag": False,
- }
- }
- }
+ },
+ },
+ },
],
"inputs": None,
"expected": {"result": "failure", "messages": ["The following port-channels have inactive port(s): ['Port-Channel42']"]},
@@ -543,12 +1292,12 @@ Et4 5:00 0.0 99.9% 0 0.0 0.0% 0
"lacpdusTxCount": 454,
"markersTxCount": 0,
"markersRxCount": 0,
- }
- }
- }
+ },
+ },
+ },
},
"orphanPorts": {},
- }
+ },
],
"inputs": None,
"expected": {"result": "success"},
@@ -570,17 +1319,17 @@ Et4 5:00 0.0 99.9% 0 0.0 0.0% 0
"lacpdusTxCount": 454,
"markersTxCount": 0,
"markersRxCount": 0,
- }
- }
- }
+ },
+ },
+ },
},
"orphanPorts": {},
- }
+ },
],
"inputs": None,
"expected": {
"result": "failure",
- "messages": ["The following port-channels have recieved illegal lacp packets on the following ports: [{'Port-Channel42': 'Ethernet8'}]"],
+ "messages": ["The following port-channels have received illegal LACP packets on the following ports: [{'Port-Channel42': 'Ethernet8'}]"],
},
},
{
@@ -605,8 +1354,8 @@ Et4 5:00 0.0 99.9% 0 0.0 0.0% 0
"lineProtocolStatus": "up",
"mtu": 65535,
},
- }
- }
+ },
+ },
],
"inputs": {"number": 2},
"expected": {"result": "success"},
@@ -633,8 +1382,8 @@ Et4 5:00 0.0 99.9% 0 0.0 0.0% 0
"lineProtocolStatus": "down",
"mtu": 65535,
},
- }
- }
+ },
+ },
],
"inputs": {"number": 2},
"expected": {"result": "failure", "messages": ["The following Loopbacks are not up: ['Loopback666']"]},
@@ -653,8 +1402,8 @@ Et4 5:00 0.0 99.9% 0 0.0 0.0% 0
"lineProtocolStatus": "up",
"mtu": 65535,
},
- }
- }
+ },
+ },
],
"inputs": {"number": 2},
"expected": {"result": "failure", "messages": ["Found 1 Loopbacks when expecting 2"]},
@@ -672,9 +1421,9 @@ Et4 5:00 0.0 99.9% 0 0.0 0.0% 0
"ipv4Routable240": False,
"lineProtocolStatus": "up",
"mtu": 1500,
- }
- }
- }
+ },
+ },
+ },
],
"inputs": None,
"expected": {"result": "success"},
@@ -692,9 +1441,9 @@ Et4 5:00 0.0 99.9% 0 0.0 0.0% 0
"ipv4Routable240": False,
"lineProtocolStatus": "lowerLayerDown",
"mtu": 1500,
- }
- }
- }
+ },
+ },
+ },
],
"inputs": None,
"expected": {"result": "failure", "messages": ["The following SVIs are not up: ['Vlan42']"]},
@@ -766,7 +1515,7 @@ Et4 5:00 0.0 99.9% 0 0.0 0.0% 0
"l2Mru": 0,
},
},
- }
+ },
],
"inputs": {"mtu": 1500},
"expected": {"result": "success"},
@@ -838,7 +1587,7 @@ Et4 5:00 0.0 99.9% 0 0.0 0.0% 0
"l2Mru": 0,
},
},
- }
+ },
],
"inputs": {"mtu": 1500, "ignored_interfaces": ["Loopback", "Port-Channel", "Management", "Vxlan"], "specific_mtu": [{"Ethernet10": 1501}]},
"expected": {"result": "success"},
@@ -910,7 +1659,7 @@ Et4 5:00 0.0 99.9% 0 0.0 0.0% 0
"l2Mru": 0,
},
},
- }
+ },
],
"inputs": {"mtu": 1500},
"expected": {"result": "failure", "messages": ["Some interfaces do not have correct MTU configured:\n[{'Ethernet2': 1600}]"]},
@@ -921,8 +1670,8 @@ Et4 5:00 0.0 99.9% 0 0.0 0.0% 0
"eos_data": [
{
"interfaces": {
- "Ethernet2": {
- "name": "Ethernet2",
+ "Ethernet2/1": {
+ "name": "Ethernet2/1",
"forwardingModel": "routed",
"lineProtocolStatus": "up",
"interfaceStatus": "connected",
@@ -982,7 +1731,7 @@ Et4 5:00 0.0 99.9% 0 0.0 0.0% 0
"l2Mru": 0,
},
},
- }
+ },
],
"inputs": {"mtu": 9214},
"expected": {"result": "success"},
@@ -1054,7 +1803,7 @@ Et4 5:00 0.0 99.9% 0 0.0 0.0% 0
"l2Mru": 0,
},
},
- }
+ },
],
"inputs": {"mtu": 1500},
"expected": {"result": "failure", "messages": ["Some L2 interfaces do not have correct MTU configured:\n[{'Ethernet10': 9214}, {'Port-Channel2': 9214}]"]},
@@ -1084,8 +1833,8 @@ Et4 5:00 0.0 99.9% 0 0.0 0.0% 0
"directedBroadcastEnabled": False,
"maxMssIngress": 0,
"maxMssEgress": 0,
- }
- }
+ },
+ },
},
{
"interfaces": {
@@ -1108,8 +1857,8 @@ Et4 5:00 0.0 99.9% 0 0.0 0.0% 0
"directedBroadcastEnabled": False,
"maxMssIngress": 0,
"maxMssEgress": 0,
- }
- }
+ },
+ },
},
],
"inputs": {"interfaces": ["Ethernet1", "Ethernet2"]},
@@ -1140,8 +1889,8 @@ Et4 5:00 0.0 99.9% 0 0.0 0.0% 0
"directedBroadcastEnabled": False,
"maxMssIngress": 0,
"maxMssEgress": 0,
- }
- }
+ },
+ },
},
{
"interfaces": {
@@ -1164,8 +1913,8 @@ Et4 5:00 0.0 99.9% 0 0.0 0.0% 0
"directedBroadcastEnabled": False,
"maxMssIngress": 0,
"maxMssEgress": 0,
- }
- }
+ },
+ },
},
],
"inputs": {"interfaces": ["Ethernet1", "Ethernet2"]},
diff --git a/tests/units/anta_tests/test_lanz.py b/tests/units/anta_tests/test_lanz.py
index 932d1ac..bfbf6ae 100644
--- a/tests/units/anta_tests/test_lanz.py
+++ b/tests/units/anta_tests/test_lanz.py
@@ -1,7 +1,8 @@
# 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.
-"""Data for testing anta.tests.configuration"""
+"""Data for testing anta.tests.lanz."""
+
from __future__ import annotations
from typing import Any
@@ -15,7 +16,7 @@ DATA: list[dict[str, Any]] = [
"test": VerifyLANZ,
"eos_data": [{"lanzEnabled": True}],
"inputs": None,
- "expected": {"result": "success", "messages": ["LANZ is enabled"]},
+ "expected": {"result": "success"},
},
{
"name": "failure",
diff --git a/tests/units/anta_tests/test_logging.py b/tests/units/anta_tests/test_logging.py
index 8ac2323..1e8ee3d 100644
--- a/tests/units/anta_tests/test_logging.py
+++ b/tests/units/anta_tests/test_logging.py
@@ -1,7 +1,8 @@
# 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.
-"""Data for testing anta.tests.logging"""
+"""Data for testing anta.tests.logging."""
+
from __future__ import annotations
from typing import Any
@@ -77,7 +78,7 @@ DATA: list[dict[str, Any]] = [
Logging to '10.22.10.93' port 514 in VRF MGMT via tcp
Logging to '10.22.10.94' port 911 in VRF MGMT via udp
- """
+ """,
],
"inputs": {"interface": "Management0", "vrf": "MGMT"},
"expected": {"result": "success"},
@@ -92,7 +93,7 @@ DATA: list[dict[str, Any]] = [
Logging to '10.22.10.93' port 514 in VRF MGMT via tcp
Logging to '10.22.10.94' port 911 in VRF MGMT via udp
- """
+ """,
],
"inputs": {"interface": "Management0", "vrf": "MGMT"},
"expected": {"result": "failure", "messages": ["Source-interface 'Management0' is not configured in VRF MGMT"]},
@@ -107,7 +108,7 @@ DATA: list[dict[str, Any]] = [
Logging to '10.22.10.93' port 514 in VRF MGMT via tcp
Logging to '10.22.10.94' port 911 in VRF MGMT via udp
- """
+ """,
],
"inputs": {"interface": "Management0", "vrf": "MGMT"},
"expected": {"result": "failure", "messages": ["Source-interface 'Management0' is not configured in VRF MGMT"]},
@@ -122,7 +123,7 @@ DATA: list[dict[str, Any]] = [
Logging to '10.22.10.93' port 514 in VRF MGMT via tcp
Logging to '10.22.10.94' port 911 in VRF MGMT via udp
- """
+ """,
],
"inputs": {"hosts": ["10.22.10.92", "10.22.10.93", "10.22.10.94"], "vrf": "MGMT"},
"expected": {"result": "success"},
@@ -137,7 +138,7 @@ DATA: list[dict[str, Any]] = [
Logging to '10.22.10.103' port 514 in VRF MGMT via tcp
Logging to '10.22.10.104' port 911 in VRF MGMT via udp
- """
+ """,
],
"inputs": {"hosts": ["10.22.10.92", "10.22.10.93", "10.22.10.94"], "vrf": "MGMT"},
"expected": {"result": "failure", "messages": ["Syslog servers ['10.22.10.93', '10.22.10.94'] are not configured in VRF MGMT"]},
@@ -152,7 +153,7 @@ DATA: list[dict[str, Any]] = [
Logging to '10.22.10.93' port 514 in VRF default via tcp
Logging to '10.22.10.94' port 911 in VRF default via udp
- """
+ """,
],
"inputs": {"hosts": ["10.22.10.92", "10.22.10.93", "10.22.10.94"], "vrf": "MGMT"},
"expected": {"result": "failure", "messages": ["Syslog servers ['10.22.10.93', '10.22.10.94'] are not configured in VRF MGMT"]},
@@ -246,7 +247,7 @@ DATA: list[dict[str, Any]] = [
"name": "failure",
"test": VerifyLoggingErrors,
"eos_data": [
- "Aug 2 19:57:42 DC1-LEAF1A Mlag: %FWK-3-SOCKET_CLOSE_REMOTE: Connection to Mlag (pid:27200) at tbt://192.168.0.1:4432/+n closed by peer (EOF)"
+ "Aug 2 19:57:42 DC1-LEAF1A Mlag: %FWK-3-SOCKET_CLOSE_REMOTE: Connection to Mlag (pid:27200) at tbt://192.168.0.1:4432/+n closed by peer (EOF)",
],
"inputs": None,
"expected": {"result": "failure", "messages": ["Device has reported syslog messages with a severity of ERRORS or higher"]},
diff --git a/tests/units/anta_tests/test_mlag.py b/tests/units/anta_tests/test_mlag.py
index 90f3c7a..ae8ff7c 100644
--- a/tests/units/anta_tests/test_mlag.py
+++ b/tests/units/anta_tests/test_mlag.py
@@ -1,9 +1,8 @@
# 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.
-"""
-Tests for anta.tests.mlag.py
-"""
+"""Tests for anta.tests.mlag.py."""
+
from __future__ import annotations
from typing import Any
@@ -25,7 +24,7 @@ DATA: list[dict[str, Any]] = [
"eos_data": [
{
"state": "disabled",
- }
+ },
],
"inputs": None,
"expected": {"result": "skipped", "messages": ["MLAG is disabled"]},
@@ -47,7 +46,7 @@ DATA: list[dict[str, Any]] = [
{
"state": "active",
"mlagPorts": {"Disabled": 0, "Configured": 0, "Inactive": 0, "Active-partial": 0, "Active-full": 1},
- }
+ },
],
"inputs": None,
"expected": {"result": "success"},
@@ -58,7 +57,7 @@ DATA: list[dict[str, Any]] = [
"eos_data": [
{
"state": "disabled",
- }
+ },
],
"inputs": None,
"expected": {"result": "skipped", "messages": ["MLAG is disabled"]},
@@ -70,7 +69,7 @@ DATA: list[dict[str, Any]] = [
{
"state": "active",
"mlagPorts": {"Disabled": 0, "Configured": 0, "Inactive": 0, "Active-partial": 1, "Active-full": 1},
- }
+ },
],
"inputs": None,
"expected": {
@@ -85,7 +84,7 @@ DATA: list[dict[str, Any]] = [
{
"state": "active",
"mlagPorts": {"Disabled": 0, "Configured": 0, "Inactive": 1, "Active-partial": 1, "Active-full": 1},
- }
+ },
],
"inputs": None,
"expected": {
@@ -106,7 +105,7 @@ DATA: list[dict[str, Any]] = [
"eos_data": [
{
"mlagActive": False,
- }
+ },
],
"inputs": None,
"expected": {"result": "skipped", "messages": ["MLAG is disabled"]},
@@ -117,7 +116,7 @@ DATA: list[dict[str, Any]] = [
"eos_data": [
{
"dummy": False,
- }
+ },
],
"inputs": None,
"expected": {"result": "error", "messages": ["Incorrect JSON response - 'mlagActive' state was not found"]},
@@ -131,7 +130,7 @@ DATA: list[dict[str, Any]] = [
"interfaceConfiguration": {},
"mlagActive": True,
"mlagConnected": True,
- }
+ },
],
"inputs": None,
"expected": {
@@ -140,7 +139,7 @@ DATA: list[dict[str, Any]] = [
"MLAG config-sanity returned inconsistencies: "
"{'globalConfiguration': {'mlag': {'globalParameters': "
"{'dual-primary-detection-delay': {'localValue': '0', 'peerValue': '200'}}}}, "
- "'interfaceConfiguration': {}}"
+ "'interfaceConfiguration': {}}",
],
},
},
@@ -153,7 +152,7 @@ DATA: list[dict[str, Any]] = [
"interfaceConfiguration": {"trunk-native-vlan mlag30": {"interface": {"Port-Channel30": {"localValue": "123", "peerValue": "3700"}}}},
"mlagActive": True,
"mlagConnected": True,
- }
+ },
],
"inputs": None,
"expected": {
@@ -162,7 +161,7 @@ DATA: list[dict[str, Any]] = [
"MLAG config-sanity returned inconsistencies: "
"{'globalConfiguration': {}, "
"'interfaceConfiguration': {'trunk-native-vlan mlag30': "
- "{'interface': {'Port-Channel30': {'localValue': '123', 'peerValue': '3700'}}}}}"
+ "{'interface': {'Port-Channel30': {'localValue': '123', 'peerValue': '3700'}}}}}",
],
},
},
@@ -179,7 +178,7 @@ DATA: list[dict[str, Any]] = [
"eos_data": [
{
"state": "disabled",
- }
+ },
],
"inputs": {"reload_delay": 300, "reload_delay_non_mlag": 330},
"expected": {"result": "skipped", "messages": ["MLAG is disabled"]},
@@ -202,7 +201,7 @@ DATA: list[dict[str, Any]] = [
"dualPrimaryMlagRecoveryDelay": 60,
"dualPrimaryNonMlagRecoveryDelay": 0,
"detail": {"dualPrimaryDetectionDelay": 200, "dualPrimaryAction": "none"},
- }
+ },
],
"inputs": {"detection_delay": 200, "errdisabled": False, "recovery_delay": 60, "recovery_delay_non_mlag": 0},
"expected": {"result": "success"},
@@ -213,7 +212,7 @@ DATA: list[dict[str, Any]] = [
"eos_data": [
{
"state": "disabled",
- }
+ },
],
"inputs": {"detection_delay": 200, "errdisabled": False, "recovery_delay": 60, "recovery_delay_non_mlag": 0},
"expected": {"result": "skipped", "messages": ["MLAG is disabled"]},
@@ -226,7 +225,7 @@ DATA: list[dict[str, Any]] = [
"state": "active",
"dualPrimaryDetectionState": "disabled",
"dualPrimaryPortsErrdisabled": False,
- }
+ },
],
"inputs": {"detection_delay": 200, "errdisabled": False, "recovery_delay": 60, "recovery_delay_non_mlag": 0},
"expected": {"result": "failure", "messages": ["Dual-primary detection is disabled"]},
@@ -242,7 +241,7 @@ DATA: list[dict[str, Any]] = [
"dualPrimaryMlagRecoveryDelay": 160,
"dualPrimaryNonMlagRecoveryDelay": 0,
"detail": {"dualPrimaryDetectionDelay": 300, "dualPrimaryAction": "none"},
- }
+ },
],
"inputs": {"detection_delay": 200, "errdisabled": False, "recovery_delay": 60, "recovery_delay_non_mlag": 0},
"expected": {
@@ -254,7 +253,7 @@ DATA: list[dict[str, Any]] = [
"'detail.dualPrimaryAction': 'none', "
"'dualPrimaryMlagRecoveryDelay': 160, "
"'dualPrimaryNonMlagRecoveryDelay': 0}"
- )
+ ),
],
},
},
@@ -269,7 +268,7 @@ DATA: list[dict[str, Any]] = [
"dualPrimaryMlagRecoveryDelay": 60,
"dualPrimaryNonMlagRecoveryDelay": 0,
"detail": {"dualPrimaryDetectionDelay": 200, "dualPrimaryAction": "none"},
- }
+ },
],
"inputs": {"detection_delay": 200, "errdisabled": True, "recovery_delay": 60, "recovery_delay_non_mlag": 0},
"expected": {
@@ -281,7 +280,7 @@ DATA: list[dict[str, Any]] = [
"'detail.dualPrimaryAction': 'none', "
"'dualPrimaryMlagRecoveryDelay': 60, "
"'dualPrimaryNonMlagRecoveryDelay': 0}"
- )
+ ),
],
},
},
diff --git a/tests/units/anta_tests/test_multicast.py b/tests/units/anta_tests/test_multicast.py
index 9276a9f..a52a1d2 100644
--- a/tests/units/anta_tests/test_multicast.py
+++ b/tests/units/anta_tests/test_multicast.py
@@ -1,7 +1,8 @@
# 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 inputs for anta.tests.multicast"""
+"""Test inputs for anta.tests.multicast."""
+
from __future__ import annotations
from typing import Any
@@ -44,7 +45,7 @@ DATA: list[dict[str, Any]] = [
"robustness": 2,
"immediateLeave": "enabled",
"reportFloodingSwitchPorts": [],
- }
+ },
],
"inputs": {"vlans": {1: True, 42: True}},
"expected": {"result": "success"},
@@ -67,12 +68,12 @@ DATA: list[dict[str, Any]] = [
"maxGroups": 65534,
"immediateLeave": "default",
"floodingTraffic": True,
- }
+ },
},
"robustness": 2,
"immediateLeave": "enabled",
"reportFloodingSwitchPorts": [],
- }
+ },
],
"inputs": {"vlans": {42: False}},
"expected": {"result": "success"},
@@ -100,7 +101,7 @@ DATA: list[dict[str, Any]] = [
"robustness": 2,
"immediateLeave": "enabled",
"reportFloodingSwitchPorts": [],
- }
+ },
],
"inputs": {"vlans": {1: False, 42: False}},
"expected": {"result": "failure", "messages": ["IGMP state for vlan 1 is enabled", "Supplied vlan 42 is not present on the device."]},
@@ -128,7 +129,7 @@ DATA: list[dict[str, Any]] = [
"robustness": 2,
"immediateLeave": "enabled",
"reportFloodingSwitchPorts": [],
- }
+ },
],
"inputs": {"vlans": {1: True}},
"expected": {"result": "failure", "messages": ["IGMP state for vlan 1 is disabled"]},
@@ -143,7 +144,7 @@ DATA: list[dict[str, Any]] = [
"robustness": 2,
"immediateLeave": "enabled",
"reportFloodingSwitchPorts": [],
- }
+ },
],
"inputs": {"enabled": True},
"expected": {"result": "success"},
@@ -155,7 +156,7 @@ DATA: list[dict[str, Any]] = [
{
"reportFlooding": "disabled",
"igmpSnoopingState": "disabled",
- }
+ },
],
"inputs": {"enabled": False},
"expected": {"result": "success"},
@@ -167,7 +168,7 @@ DATA: list[dict[str, Any]] = [
{
"reportFlooding": "disabled",
"igmpSnoopingState": "disabled",
- }
+ },
],
"inputs": {"enabled": True},
"expected": {"result": "failure", "messages": ["IGMP state is not valid: disabled"]},
diff --git a/tests/units/anta_tests/test_profiles.py b/tests/units/anta_tests/test_profiles.py
index c0ebb57..d58e987 100644
--- a/tests/units/anta_tests/test_profiles.py
+++ b/tests/units/anta_tests/test_profiles.py
@@ -1,9 +1,8 @@
# 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.
-"""
-Tests for anta.tests.profiles.py
-"""
+"""Tests for anta.tests.profiles.py."""
+
from __future__ import annotations
from typing import Any
@@ -30,7 +29,7 @@ DATA: list[dict[str, Any]] = [
"name": "success",
"test": VerifyTcamProfile,
"eos_data": [
- {"pmfProfiles": {"FixedSystem": {"config": "test", "configType": "System Profile", "status": "test", "mode": "tcam"}}, "lastProgrammingStatus": {}}
+ {"pmfProfiles": {"FixedSystem": {"config": "test", "configType": "System Profile", "status": "test", "mode": "tcam"}}, "lastProgrammingStatus": {}},
],
"inputs": {"profile": "test"},
"expected": {"result": "success"},
@@ -39,7 +38,7 @@ DATA: list[dict[str, Any]] = [
"name": "failure",
"test": VerifyTcamProfile,
"eos_data": [
- {"pmfProfiles": {"FixedSystem": {"config": "test", "configType": "System Profile", "status": "default", "mode": "tcam"}}, "lastProgrammingStatus": {}}
+ {"pmfProfiles": {"FixedSystem": {"config": "test", "configType": "System Profile", "status": "default", "mode": "tcam"}}, "lastProgrammingStatus": {}},
],
"inputs": {"profile": "test"},
"expected": {"result": "failure", "messages": ["Incorrect profile running on device: default"]},
diff --git a/tests/units/anta_tests/test_ptp.py b/tests/units/anta_tests/test_ptp.py
index 3969c97..ef42a58 100644
--- a/tests/units/anta_tests/test_ptp.py
+++ b/tests/units/anta_tests/test_ptp.py
@@ -1,17 +1,19 @@
# 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.
-"""Data for testing anta.tests.configuration"""
+"""Data for testing anta.tests.ptp."""
+
from __future__ import annotations
from typing import Any
-from anta.tests.ptp import VerifyPtpStatus
+from anta.tests.ptp import VerifyPtpGMStatus, VerifyPtpLockStatus, VerifyPtpModeStatus, VerifyPtpOffset, VerifyPtpPortModeStatus
+from tests.lib.anta import test # noqa: F401; pylint: disable=W0611
DATA: list[dict[str, Any]] = [
{
"name": "success",
- "test": VerifyPtpStatus,
+ "test": VerifyPtpModeStatus,
"eos_data": [
{
"ptpMode": "ptpBoundaryClock",
@@ -34,9 +36,305 @@ DATA: list[dict[str, Any]] = [
},
{
"name": "failure",
- "test": VerifyPtpStatus,
+ "test": VerifyPtpModeStatus,
+ "eos_data": [{"ptpMode": "ptpDisabled", "ptpIntfSummaries": {}}],
+ "inputs": None,
+ "expected": {"result": "failure", "messages": ["The device is not configured as a PTP Boundary Clock: 'ptpDisabled'"]},
+ },
+ {
+ "name": "error",
+ "test": VerifyPtpModeStatus,
+ "eos_data": [{"ptpIntfSummaries": {}}],
+ "inputs": None,
+ "expected": {"result": "error", "messages": ["'ptpMode' variable is not present in the command output"]},
+ },
+ {
+ "name": "success",
+ "test": VerifyPtpGMStatus,
+ "eos_data": [
+ {
+ "ptpMode": "ptpBoundaryClock",
+ "ptpProfile": "ptpDefaultProfile",
+ "ptpClockSummary": {
+ "clockIdentity": "0x00:1c:73:ff:ff:14:00:01",
+ "gmClockIdentity": "0xec:46:70:ff:fe:00:ff:a8",
+ "numberOfSlavePorts": 1,
+ "numberOfMasterPorts": 8,
+ "slavePort": "Ethernet27/1",
+ "slaveVlanId": 0,
+ "offsetFromMaster": -11,
+ "meanPathDelay": 105,
+ "stepsRemoved": 2,
+ "skew": 1.0000015265007687,
+ "lastSyncTime": 1708599750,
+ "currentPtpSystemTime": 1708599750,
+ },
+ }
+ ],
+ "inputs": {"gmid": "0xec:46:70:ff:fe:00:ff:a8"},
+ "expected": {"result": "success"},
+ },
+ {
+ "name": "failure",
+ "test": VerifyPtpGMStatus,
+ "eos_data": [
+ {
+ "ptpMode": "ptpBoundaryClock",
+ "ptpProfile": "ptpDefaultProfile",
+ "ptpClockSummary": {
+ "clockIdentity": "0x00:1c:73:ff:ff:0a:00:01",
+ "gmClockIdentity": "0x00:1c:73:ff:ff:0a:00:01",
+ "numberOfSlavePorts": 0,
+ "numberOfMasterPorts": 4,
+ "offsetFromMaster": 3,
+ "meanPathDelay": 496,
+ "stepsRemoved": 0,
+ "skew": 1.0000074628720317,
+ "lastSyncTime": 1708600129,
+ "currentPtpSystemTime": 1708600153,
+ },
+ }
+ ],
+ "inputs": {"gmid": "0xec:46:70:ff:fe:00:ff:a8"},
+ "expected": {
+ "result": "failure",
+ "messages": [
+ "The device is locked to the following Grandmaster: '0x00:1c:73:ff:ff:0a:00:01', which differ from the expected one.",
+ ],
+ },
+ },
+ {
+ "name": "error",
+ "test": VerifyPtpGMStatus,
+ "eos_data": [{"ptpIntfSummaries": {}}],
+ "inputs": {"gmid": "0xec:46:70:ff:fe:00:ff:a8"},
+ "expected": {"result": "error", "messages": ["'ptpClockSummary' variable is not present in the command output"]},
+ },
+ {
+ "name": "success",
+ "test": VerifyPtpLockStatus,
+ "eos_data": [
+ {
+ "ptpMode": "ptpBoundaryClock",
+ "ptpProfile": "ptpDefaultProfile",
+ "ptpClockSummary": {
+ "clockIdentity": "0x00:1c:73:ff:ff:14:00:01",
+ "gmClockIdentity": "0xec:46:70:ff:fe:00:ff:a8",
+ "numberOfSlavePorts": 1,
+ "numberOfMasterPorts": 8,
+ "slavePort": "Ethernet27/1",
+ "slaveVlanId": 0,
+ "offsetFromMaster": -11,
+ "meanPathDelay": 105,
+ "stepsRemoved": 2,
+ "skew": 1.0000015265007687,
+ "lastSyncTime": 1708599750,
+ "currentPtpSystemTime": 1708599750,
+ },
+ }
+ ],
+ "inputs": None,
+ "expected": {"result": "success"},
+ },
+ {
+ "name": "failure",
+ "test": VerifyPtpLockStatus,
+ "eos_data": [
+ {
+ "ptpMode": "ptpBoundaryClock",
+ "ptpProfile": "ptpDefaultProfile",
+ "ptpClockSummary": {
+ "clockIdentity": "0x00:1c:73:ff:ff:0a:00:01",
+ "gmClockIdentity": "0x00:1c:73:ff:ff:0a:00:01",
+ "numberOfSlavePorts": 0,
+ "numberOfMasterPorts": 4,
+ "offsetFromMaster": 3,
+ "meanPathDelay": 496,
+ "stepsRemoved": 0,
+ "skew": 1.0000074628720317,
+ "lastSyncTime": 1708600129,
+ "currentPtpSystemTime": 1708600286,
+ },
+ }
+ ],
+ "inputs": None,
+ "expected": {"result": "failure", "messages": ["The device lock is more than 60s old: 157s"]},
+ },
+ {
+ "name": "error",
+ "test": VerifyPtpLockStatus,
+ "eos_data": [{"ptpIntfSummaries": {}}],
+ "inputs": None,
+ "expected": {
+ "result": "error",
+ "messages": [
+ "'ptpClockSummary' variable is not present in the command output",
+ ],
+ },
+ },
+ {
+ "name": "success",
+ "test": VerifyPtpOffset,
+ "eos_data": [
+ {
+ "monitorEnabled": True,
+ "ptpMode": "ptpBoundaryClock",
+ "offsetFromMasterThreshold": 250,
+ "meanPathDelayThreshold": 1500,
+ "ptpMonitorData": [
+ {
+ "intf": "Ethernet27/1",
+ "realLastSyncTime": 1708599815611398400,
+ "lastSyncSeqId": 44413,
+ "offsetFromMaster": 2,
+ "meanPathDelay": 105,
+ "skew": 1.000001614,
+ },
+ {
+ "intf": "Ethernet27/1",
+ "realLastSyncTime": 1708599815486101500,
+ "lastSyncSeqId": 44412,
+ "offsetFromMaster": -13,
+ "meanPathDelay": 105,
+ "skew": 1.000001614,
+ },
+ ],
+ }
+ ],
+ "inputs": None,
+ "expected": {"result": "success"},
+ },
+ {
+ "name": "failure",
+ "test": VerifyPtpOffset,
+ "eos_data": [
+ {
+ "monitorEnabled": True,
+ "ptpMode": "ptpBoundaryClock",
+ "offsetFromMasterThreshold": 250,
+ "meanPathDelayThreshold": 1500,
+ "ptpMonitorData": [
+ {
+ "intf": "Ethernet27/1",
+ "realLastSyncTime": 1708599815611398400,
+ "lastSyncSeqId": 44413,
+ "offsetFromMaster": 1200,
+ "meanPathDelay": 105,
+ "skew": 1.000001614,
+ },
+ {
+ "intf": "Ethernet27/1",
+ "realLastSyncTime": 1708599815486101500,
+ "lastSyncSeqId": 44412,
+ "offsetFromMaster": -1300,
+ "meanPathDelay": 105,
+ "skew": 1.000001614,
+ },
+ ],
+ }
+ ],
+ "inputs": None,
+ "expected": {
+ "result": "failure",
+ "messages": [("The device timing offset from master is greater than +/- 1000ns: {'Ethernet27/1': [1200, -1300]}")],
+ },
+ },
+ {
+ "name": "skipped",
+ "test": VerifyPtpOffset,
+ "eos_data": [
+ {
+ "monitorEnabled": True,
+ "ptpMonitorData": [],
+ },
+ ],
+ "inputs": None,
+ "expected": {"result": "skipped", "messages": ["PTP is not configured"]},
+ },
+ {
+ "name": "success",
+ "test": VerifyPtpPortModeStatus,
+ "eos_data": [
+ {
+ "ptpMode": "ptpBoundaryClock",
+ "ptpProfile": "ptpDefaultProfile",
+ "ptpClockSummary": {
+ "clockIdentity": "0x00:1c:73:ff:ff:0a:00:01",
+ "gmClockIdentity": "0x00:1c:73:ff:ff:0a:00:01",
+ "numberOfSlavePorts": 0,
+ "numberOfMasterPorts": 4,
+ "offsetFromMaster": 0,
+ "meanPathDelay": 0,
+ "stepsRemoved": 0,
+ "skew": 1.0,
+ },
+ "ptpIntfSummaries": {
+ "Ethernet53": {
+ "interface": "Ethernet53",
+ "ptpIntfVlanSummaries": [
+ {
+ "vlanId": 0,
+ "portState": "psDisabled",
+ "delayMechanism": "e2e",
+ "transportMode": "ipv4",
+ "mpassEnabled": False,
+ "mpassStatus": "active",
+ }
+ ],
+ },
+ "Ethernet1": {
+ "interface": "Ethernet1",
+ "ptpIntfVlanSummaries": [
+ {"vlanId": 0, "portState": "psMaster", "delayMechanism": "e2e", "transportMode": "ipv4", "mpassEnabled": False, "mpassStatus": "active"}
+ ],
+ },
+ },
+ }
+ ],
+ "inputs": None,
+ "expected": {"result": "success"},
+ },
+ {
+ "name": "failure",
+ "test": VerifyPtpPortModeStatus,
"eos_data": [{"ptpIntfSummaries": {}}],
"inputs": None,
- "expected": {"result": "failure"},
+ "expected": {"result": "failure", "messages": ["No interfaces are PTP enabled"]},
+ },
+ {
+ "name": "failure",
+ "test": VerifyPtpPortModeStatus,
+ "eos_data": [
+ {
+ "ptpMode": "ptpBoundaryClock",
+ "ptpProfile": "ptpDefaultProfile",
+ "ptpClockSummary": {
+ "clockIdentity": "0x00:1c:73:ff:ff:0a:00:01",
+ "gmClockIdentity": "0x00:1c:73:ff:ff:0a:00:01",
+ "numberOfSlavePorts": 0,
+ "numberOfMasterPorts": 4,
+ "offsetFromMaster": 0,
+ "meanPathDelay": 0,
+ "stepsRemoved": 0,
+ "skew": 1.0,
+ },
+ "ptpIntfSummaries": {
+ "Ethernet53": {
+ "interface": "Ethernet53",
+ "ptpIntfVlanSummaries": [
+ {"vlanId": 0, "portState": "none", "delayMechanism": "e2e", "transportMode": "ipv4", "mpassEnabled": False, "mpassStatus": "active"}
+ ],
+ },
+ "Ethernet1": {
+ "interface": "Ethernet1",
+ "ptpIntfVlanSummaries": [
+ {"vlanId": 0, "portState": "none", "delayMechanism": "e2e", "transportMode": "ipv4", "mpassEnabled": False, "mpassStatus": "active"}
+ ],
+ },
+ },
+ }
+ ],
+ "inputs": None,
+ "expected": {"result": "failure", "messages": ["The following interface(s) are not in a valid PTP state: '['Ethernet53', 'Ethernet1']'"]},
},
]
diff --git a/tests/units/anta_tests/test_security.py b/tests/units/anta_tests/test_security.py
index 17fa04e..4c28541 100644
--- a/tests/units/anta_tests/test_security.py
+++ b/tests/units/anta_tests/test_security.py
@@ -1,9 +1,8 @@
# 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.
-"""
-Tests for anta.tests.security.py
-"""
+"""Tests for anta.tests.security.py."""
+
from __future__ import annotations
from typing import Any
@@ -16,7 +15,9 @@ from anta.tests.security import (
VerifyAPISSLCertificate,
VerifyBannerLogin,
VerifyBannerMotd,
+ VerifyIPSecConnHealth,
VerifyIPv4ACL,
+ VerifySpecificIPSecConn,
VerifySSHIPv4Acl,
VerifySSHIPv6Acl,
VerifySSHStatus,
@@ -107,7 +108,7 @@ DATA: list[dict[str, Any]] = [
"unixSocketServer": {"configured": False, "running": False},
"sslProfile": {"name": "API_SSL_Profile", "configured": True, "state": "valid"},
"tlsProtocol": ["1.2"],
- }
+ },
],
"inputs": None,
"expected": {"result": "success"},
@@ -124,7 +125,7 @@ DATA: list[dict[str, Any]] = [
"unixSocketServer": {"configured": False, "running": False},
"sslProfile": {"name": "API_SSL_Profile", "configured": True, "state": "valid"},
"tlsProtocol": ["1.2"],
- }
+ },
],
"inputs": None,
"expected": {"result": "failure", "messages": ["eAPI HTTP server is enabled globally"]},
@@ -141,7 +142,7 @@ DATA: list[dict[str, Any]] = [
"unixSocketServer": {"configured": False, "running": False},
"sslProfile": {"name": "API_SSL_Profile", "configured": True, "state": "valid"},
"tlsProtocol": ["1.2"],
- }
+ },
],
"inputs": {"profile": "API_SSL_Profile"},
"expected": {"result": "success"},
@@ -157,7 +158,7 @@ DATA: list[dict[str, Any]] = [
"httpsServer": {"configured": True, "running": True, "port": 443},
"unixSocketServer": {"configured": False, "running": False},
"tlsProtocol": ["1.2"],
- }
+ },
],
"inputs": {"profile": "API_SSL_Profile"},
"expected": {"result": "failure", "messages": ["eAPI HTTPS server SSL profile (API_SSL_Profile) is not configured"]},
@@ -174,7 +175,7 @@ DATA: list[dict[str, Any]] = [
"unixSocketServer": {"configured": False, "running": False},
"sslProfile": {"name": "Wrong_SSL_Profile", "configured": True, "state": "valid"},
"tlsProtocol": ["1.2"],
- }
+ },
],
"inputs": {"profile": "API_SSL_Profile"},
"expected": {"result": "failure", "messages": ["eAPI HTTPS server SSL profile (API_SSL_Profile) is misconfigured or invalid"]},
@@ -897,4 +898,278 @@ DATA: list[dict[str, Any]] = [
],
},
},
+ {
+ "name": "success",
+ "test": VerifyIPSecConnHealth,
+ "eos_data": [
+ {
+ "connections": {
+ "default-172.18.3.2-172.18.5.2-srcUnused-0": {
+ "pathDict": {"path9": "Established"},
+ },
+ "default-100.64.3.2-100.64.5.2-srcUnused-0": {
+ "pathDict": {"path10": "Established"},
+ },
+ }
+ }
+ ],
+ "inputs": {},
+ "expected": {"result": "success"},
+ },
+ {
+ "name": "failure-no-connection",
+ "test": VerifyIPSecConnHealth,
+ "eos_data": [{"connections": {}}],
+ "inputs": {},
+ "expected": {"result": "failure", "messages": ["No IPv4 security connection configured."]},
+ },
+ {
+ "name": "failure-not-established",
+ "test": VerifyIPSecConnHealth,
+ "eos_data": [
+ {
+ "connections": {
+ "default-172.18.3.2-172.18.5.2-srcUnused-0": {
+ "pathDict": {"path9": "Idle"},
+ "saddr": "172.18.3.2",
+ "daddr": "172.18.2.2",
+ "tunnelNs": "default",
+ },
+ "Guest-100.64.3.2-100.64.5.2-srcUnused-0": {"pathDict": {"path10": "Idle"}, "saddr": "100.64.3.2", "daddr": "100.64.5.2", "tunnelNs": "Guest"},
+ }
+ }
+ ],
+ "inputs": {},
+ "expected": {
+ "result": "failure",
+ "messages": [
+ "The following IPv4 security connections are not established:\n"
+ "source:172.18.3.2 destination:172.18.2.2 vrf:default\n"
+ "source:100.64.3.2 destination:100.64.5.2 vrf:Guest."
+ ],
+ },
+ },
+ {
+ "name": "success-with-connection",
+ "test": VerifySpecificIPSecConn,
+ "eos_data": [
+ {
+ "connections": {
+ "Guest-172.18.3.2-172.18.2.2-srcUnused-0": {
+ "pathDict": {"path9": "Established"},
+ "saddr": "172.18.3.2",
+ "daddr": "172.18.2.2",
+ "tunnelNs": "Guest",
+ },
+ "Guest-100.64.3.2-100.64.2.2-srcUnused-0": {
+ "pathDict": {"path10": "Established"},
+ "saddr": "100.64.3.2",
+ "daddr": "100.64.2.2",
+ "tunnelNs": "Guest",
+ },
+ }
+ }
+ ],
+ "inputs": {
+ "ip_security_connections": [
+ {
+ "peer": "10.255.0.1",
+ "vrf": "Guest",
+ "connections": [
+ {"source_address": "100.64.3.2", "destination_address": "100.64.2.2"},
+ {"source_address": "172.18.3.2", "destination_address": "172.18.2.2"},
+ ],
+ },
+ ]
+ },
+ "expected": {"result": "success"},
+ },
+ {
+ "name": "success-without-connection",
+ "test": VerifySpecificIPSecConn,
+ "eos_data": [
+ {
+ "connections": {
+ "default-172.18.3.2-172.18.2.2-srcUnused-0": {
+ "pathDict": {"path9": "Established"},
+ "saddr": "172.18.3.2",
+ "daddr": "172.18.2.2",
+ "tunnelNs": "default",
+ },
+ "default-100.64.3.2-100.64.2.2-srcUnused-0": {"pathDict": {"path10": "Established"}, "saddr": "100.64.3.2", "daddr": "100.64.2.2"},
+ }
+ }
+ ],
+ "inputs": {
+ "ip_security_connections": [
+ {
+ "peer": "10.255.0.1",
+ "vrf": "default",
+ },
+ ]
+ },
+ "expected": {"result": "success"},
+ },
+ {
+ "name": "failure-no-connection",
+ "test": VerifySpecificIPSecConn,
+ "eos_data": [
+ {"connections": {}},
+ {
+ "connections": {
+ "DATA-172.18.3.2-172.18.2.2-srcUnused-0": {
+ "pathDict": {"path9": "Established"},
+ "saddr": "172.18.3.2",
+ "daddr": "172.18.2.2",
+ "tunnelNs": "DATA",
+ },
+ "DATA-100.64.3.2-100.64.2.2-srcUnused-0": {
+ "pathDict": {"path10": "Established"},
+ "saddr": "100.64.3.2",
+ "daddr": "100.64.2.2",
+ "tunnelNs": "DATA",
+ },
+ }
+ },
+ ],
+ "inputs": {
+ "ip_security_connections": [
+ {
+ "peer": "10.255.0.1",
+ "vrf": "default",
+ },
+ {
+ "peer": "10.255.0.2",
+ "vrf": "DATA",
+ "connections": [
+ {"source_address": "100.64.3.2", "destination_address": "100.64.2.2"},
+ {"source_address": "172.18.3.2", "destination_address": "172.18.2.2"},
+ ],
+ },
+ ]
+ },
+ "expected": {"result": "failure", "messages": ["No IPv4 security connection configured for peer `10.255.0.1`."]},
+ },
+ {
+ "name": "failure-not-established",
+ "test": VerifySpecificIPSecConn,
+ "eos_data": [
+ {
+ "connections": {
+ "default-172.18.3.2-172.18.5.2-srcUnused-0": {
+ "pathDict": {"path9": "Idle"},
+ "saddr": "172.18.3.2",
+ "daddr": "172.18.2.2",
+ "tunnelNs": "default",
+ },
+ "default-100.64.3.2-100.64.5.2-srcUnused-0": {
+ "pathDict": {"path10": "Idle"},
+ "saddr": "100.64.2.2",
+ "daddr": "100.64.1.2",
+ "tunnelNs": "default",
+ },
+ },
+ },
+ {
+ "connections": {
+ "MGMT-172.18.2.2-172.18.1.2-srcUnused-0": {"pathDict": {"path9": "Idle"}, "saddr": "172.18.2.2", "daddr": "172.18.1.2", "tunnelNs": "MGMT"},
+ "MGMT-100.64.2.2-100.64.1.2-srcUnused-0": {"pathDict": {"path10": "Idle"}, "saddr": "100.64.2.2", "daddr": "100.64.1.2", "tunnelNs": "MGMT"},
+ }
+ },
+ ],
+ "inputs": {
+ "ip_security_connections": [
+ {
+ "peer": "10.255.0.1",
+ "vrf": "default",
+ },
+ {
+ "peer": "10.255.0.2",
+ "vrf": "MGMT",
+ "connections": [
+ {"source_address": "100.64.2.2", "destination_address": "100.64.1.2"},
+ {"source_address": "172.18.2.2", "destination_address": "172.18.1.2"},
+ ],
+ },
+ ]
+ },
+ "expected": {
+ "result": "failure",
+ "messages": [
+ "Expected state of IPv4 security connection `source:172.18.3.2 destination:172.18.2.2 vrf:default` for peer `10.255.0.1` is `Established` "
+ "but found `Idle` instead.",
+ "Expected state of IPv4 security connection `source:100.64.2.2 destination:100.64.1.2 vrf:default` for peer `10.255.0.1` is `Established` "
+ "but found `Idle` instead.",
+ "Expected state of IPv4 security connection `source:100.64.2.2 destination:100.64.1.2 vrf:MGMT` for peer `10.255.0.2` is `Established` "
+ "but found `Idle` instead.",
+ "Expected state of IPv4 security connection `source:172.18.2.2 destination:172.18.1.2 vrf:MGMT` for peer `10.255.0.2` is `Established` "
+ "but found `Idle` instead.",
+ ],
+ },
+ },
+ {
+ "name": "failure-missing-connection",
+ "test": VerifySpecificIPSecConn,
+ "eos_data": [
+ {
+ "connections": {
+ "default-172.18.3.2-172.18.5.2-srcUnused-0": {
+ "pathDict": {"path9": "Idle"},
+ "saddr": "172.18.3.2",
+ "daddr": "172.18.2.2",
+ "tunnelNs": "default",
+ },
+ "default-100.64.3.2-100.64.5.2-srcUnused-0": {
+ "pathDict": {"path10": "Idle"},
+ "saddr": "100.64.3.2",
+ "daddr": "100.64.2.2",
+ "tunnelNs": "default",
+ },
+ },
+ },
+ {
+ "connections": {
+ "default-172.18.2.2-172.18.1.2-srcUnused-0": {
+ "pathDict": {"path9": "Idle"},
+ "saddr": "172.18.2.2",
+ "daddr": "172.18.1.2",
+ "tunnelNs": "default",
+ },
+ "default-100.64.2.2-100.64.1.2-srcUnused-0": {
+ "pathDict": {"path10": "Idle"},
+ "saddr": "100.64.2.2",
+ "daddr": "100.64.1.2",
+ "tunnelNs": "default",
+ },
+ }
+ },
+ ],
+ "inputs": {
+ "ip_security_connections": [
+ {
+ "peer": "10.255.0.1",
+ "vrf": "default",
+ },
+ {
+ "peer": "10.255.0.2",
+ "vrf": "default",
+ "connections": [
+ {"source_address": "100.64.4.2", "destination_address": "100.64.1.2"},
+ {"source_address": "172.18.4.2", "destination_address": "172.18.1.2"},
+ ],
+ },
+ ]
+ },
+ "expected": {
+ "result": "failure",
+ "messages": [
+ "Expected state of IPv4 security connection `source:172.18.3.2 destination:172.18.2.2 vrf:default` for peer `10.255.0.1` is `Established` "
+ "but found `Idle` instead.",
+ "Expected state of IPv4 security connection `source:100.64.3.2 destination:100.64.2.2 vrf:default` for peer `10.255.0.1` is `Established` "
+ "but found `Idle` instead.",
+ "IPv4 security connection `source:100.64.4.2 destination:100.64.1.2 vrf:default` for peer `10.255.0.2` is not found.",
+ "IPv4 security connection `source:172.18.4.2 destination:172.18.1.2 vrf:default` for peer `10.255.0.2` is not found.",
+ ],
+ },
+ },
]
diff --git a/tests/units/anta_tests/test_services.py b/tests/units/anta_tests/test_services.py
index dcd1ee2..ed86e10 100644
--- a/tests/units/anta_tests/test_services.py
+++ b/tests/units/anta_tests/test_services.py
@@ -1,9 +1,8 @@
# 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.
-"""
-Tests for anta.tests.services.py
-"""
+"""Tests for anta.tests.services.py."""
+
from __future__ import annotations
from typing import Any
diff --git a/tests/units/anta_tests/test_snmp.py b/tests/units/anta_tests/test_snmp.py
index 7009689..b4d3152 100644
--- a/tests/units/anta_tests/test_snmp.py
+++ b/tests/units/anta_tests/test_snmp.py
@@ -1,9 +1,8 @@
# 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.
-"""
-Tests for anta.tests.snmp.py
-"""
+"""Tests for anta.tests.snmp.py."""
+
from __future__ import annotations
from typing import Any
diff --git a/tests/units/anta_tests/test_software.py b/tests/units/anta_tests/test_software.py
index 6d39c04..84e90e8 100644
--- a/tests/units/anta_tests/test_software.py
+++ b/tests/units/anta_tests/test_software.py
@@ -1,7 +1,8 @@
# 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 inputs for anta.tests.hardware"""
+"""Test inputs for anta.tests.hardware."""
+
from __future__ import annotations
from typing import Any
@@ -18,7 +19,7 @@ DATA: list[dict[str, Any]] = [
"modelName": "vEOS-lab",
"internalVersion": "4.27.0F-24305004.4270F",
"version": "4.27.0F",
- }
+ },
],
"inputs": {"versions": ["4.27.0F", "4.28.0F"]},
"expected": {"result": "success"},
@@ -31,7 +32,7 @@ DATA: list[dict[str, Any]] = [
"modelName": "vEOS-lab",
"internalVersion": "4.27.0F-24305004.4270F",
"version": "4.27.0F",
- }
+ },
],
"inputs": {"versions": ["4.27.1F"]},
"expected": {"result": "failure", "messages": ["device is running version \"4.27.0F\" not in expected versions: ['4.27.1F']"]},
@@ -52,7 +53,7 @@ DATA: list[dict[str, Any]] = [
"TerminAttr-core": {"release": "1", "version": "v1.17.0"},
},
},
- }
+ },
],
"inputs": {"versions": ["v1.17.0", "v1.18.1"]},
"expected": {"result": "success"},
@@ -73,7 +74,7 @@ DATA: list[dict[str, Any]] = [
"TerminAttr-core": {"release": "1", "version": "v1.17.0"},
},
},
- }
+ },
],
"inputs": {"versions": ["v1.17.1", "v1.18.1"]},
"expected": {"result": "failure", "messages": ["device is running TerminAttr version v1.17.0 and is not in the allowed list: ['v1.17.1', 'v1.18.1']"]},
diff --git a/tests/units/anta_tests/test_stp.py b/tests/units/anta_tests/test_stp.py
index 26f0b90..64a1168 100644
--- a/tests/units/anta_tests/test_stp.py
+++ b/tests/units/anta_tests/test_stp.py
@@ -1,9 +1,8 @@
# 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.
-"""
-Tests for anta.tests.stp.py
-"""
+"""Tests for anta.tests.stp.py."""
+
from __future__ import annotations
from typing import Any
@@ -84,8 +83,8 @@ DATA: list[dict[str, Any]] = [
"interfaces": {
"Ethernet10": {"bpduSent": 201, "bpduReceived": 0, "bpduTaggedError": 3, "bpduOtherError": 0, "bpduRateLimitCount": 0},
"Ethernet11": {"bpduSent": 99, "bpduReceived": 0, "bpduTaggedError": 0, "bpduOtherError": 6, "bpduRateLimitCount": 0},
- }
- }
+ },
+ },
],
"inputs": None,
"expected": {"result": "failure", "messages": ["The following interfaces have STP BPDU packet errors: ['Ethernet10', 'Ethernet11']"]},
@@ -145,7 +144,7 @@ DATA: list[dict[str, Any]] = [
"inputs": {"vlans": [10, 20]},
"expected": {
"result": "failure",
- "messages": ["The following VLAN(s) have interface(s) that are not in a fowarding state: [{'VLAN 10': ['Ethernet10']}, {'VLAN 20': ['Ethernet10']}]"],
+ "messages": ["The following VLAN(s) have interface(s) that are not in a forwarding state: [{'VLAN 10': ['Ethernet10']}, {'VLAN 20': ['Ethernet10']}]"],
},
},
{
@@ -162,7 +161,7 @@ DATA: list[dict[str, Any]] = [
"helloTime": 2.0,
"maxAge": 20,
"forwardDelay": 15,
- }
+ },
},
"VL20": {
"rootBridge": {
@@ -172,7 +171,7 @@ DATA: list[dict[str, Any]] = [
"helloTime": 2.0,
"maxAge": 20,
"forwardDelay": 15,
- }
+ },
},
"VL30": {
"rootBridge": {
@@ -182,10 +181,10 @@ DATA: list[dict[str, Any]] = [
"helloTime": 2.0,
"maxAge": 20,
"forwardDelay": 15,
- }
+ },
},
- }
- }
+ },
+ },
],
"inputs": {"priority": 32768, "instances": [10, 20]},
"expected": {"result": "success"},
@@ -204,7 +203,7 @@ DATA: list[dict[str, Any]] = [
"helloTime": 2.0,
"maxAge": 20,
"forwardDelay": 15,
- }
+ },
},
"VL20": {
"rootBridge": {
@@ -214,7 +213,7 @@ DATA: list[dict[str, Any]] = [
"helloTime": 2.0,
"maxAge": 20,
"forwardDelay": 15,
- }
+ },
},
"VL30": {
"rootBridge": {
@@ -224,10 +223,10 @@ DATA: list[dict[str, Any]] = [
"helloTime": 2.0,
"maxAge": 20,
"forwardDelay": 15,
- }
+ },
},
- }
- }
+ },
+ },
],
"inputs": {"priority": 32768},
"expected": {"result": "success"},
@@ -246,10 +245,10 @@ DATA: list[dict[str, Any]] = [
"helloTime": 2.0,
"maxAge": 20,
"forwardDelay": 15,
- }
- }
- }
- }
+ },
+ },
+ },
+ },
],
"inputs": {"priority": 16384, "instances": [0]},
"expected": {"result": "success"},
@@ -268,10 +267,10 @@ DATA: list[dict[str, Any]] = [
"helloTime": 2.0,
"maxAge": 20,
"forwardDelay": 15,
- }
- }
- }
- }
+ },
+ },
+ },
+ },
],
"inputs": {"priority": 32768, "instances": [0]},
"expected": {"result": "failure", "messages": ["Unsupported STP instance type: WRONG0"]},
@@ -297,7 +296,7 @@ DATA: list[dict[str, Any]] = [
"helloTime": 2.0,
"maxAge": 20,
"forwardDelay": 15,
- }
+ },
},
"VL20": {
"rootBridge": {
@@ -307,7 +306,7 @@ DATA: list[dict[str, Any]] = [
"helloTime": 2.0,
"maxAge": 20,
"forwardDelay": 15,
- }
+ },
},
"VL30": {
"rootBridge": {
@@ -317,10 +316,10 @@ DATA: list[dict[str, Any]] = [
"helloTime": 2.0,
"maxAge": 20,
"forwardDelay": 15,
- }
+ },
},
- }
- }
+ },
+ },
],
"inputs": {"priority": 32768, "instances": [10, 20, 30]},
"expected": {"result": "failure", "messages": ["The following instance(s) have the wrong STP root priority configured: ['VL20', 'VL30']"]},
diff --git a/tests/units/anta_tests/test_stun.py b/tests/units/anta_tests/test_stun.py
new file mode 100644
index 0000000..2c87365
--- /dev/null
+++ b/tests/units/anta_tests/test_stun.py
@@ -0,0 +1,176 @@
+# 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 inputs for anta.tests.stun.py."""
+
+from __future__ import annotations
+
+from typing import Any
+
+from anta.tests.stun import VerifyStunClient
+from tests.lib.anta import test # noqa: F401; pylint: disable=W0611
+
+DATA: list[dict[str, Any]] = [
+ {
+ "name": "success",
+ "test": VerifyStunClient,
+ "eos_data": [
+ {
+ "bindings": {
+ "000000010a64ff0100000000": {
+ "sourceAddress": {"ip": "100.64.3.2", "port": 4500},
+ "publicAddress": {"ip": "192.64.3.2", "port": 6006},
+ }
+ }
+ },
+ {
+ "bindings": {
+ "000000040a64ff0100000000": {
+ "sourceAddress": {"ip": "172.18.3.2", "port": 4500},
+ "publicAddress": {"ip": "192.18.3.2", "port": 6006},
+ }
+ }
+ },
+ {
+ "bindings": {
+ "000000040a64ff0100000000": {
+ "sourceAddress": {"ip": "172.18.4.2", "port": 4500},
+ "publicAddress": {"ip": "192.18.4.2", "port": 6006},
+ }
+ }
+ },
+ {
+ "bindings": {
+ "000000040a64ff0100000000": {
+ "sourceAddress": {"ip": "172.18.6.2", "port": 4500},
+ "publicAddress": {"ip": "192.18.6.2", "port": 6006},
+ }
+ }
+ },
+ ],
+ "inputs": {
+ "stun_clients": [
+ {"source_address": "100.64.3.2", "public_address": "192.64.3.2", "source_port": 4500, "public_port": 6006},
+ {"source_address": "172.18.3.2"},
+ {"source_address": "172.18.4.2", "source_port": 4500, "public_address": "192.18.4.2"},
+ {"source_address": "172.18.6.2", "source_port": 4500, "public_port": 6006},
+ ]
+ },
+ "expected": {"result": "success"},
+ },
+ {
+ "name": "failure-incorrect-public-ip",
+ "test": VerifyStunClient,
+ "eos_data": [
+ {
+ "bindings": {
+ "000000010a64ff0100000000": {
+ "sourceAddress": {"ip": "100.64.3.2", "port": 4500},
+ "publicAddress": {"ip": "192.64.3.2", "port": 6006},
+ }
+ }
+ },
+ {
+ "bindings": {
+ "000000040a64ff0100000000": {
+ "sourceAddress": {"ip": "172.18.3.2", "port": 4500},
+ "publicAddress": {"ip": "192.18.3.2", "port": 6006},
+ }
+ }
+ },
+ ],
+ "inputs": {
+ "stun_clients": [
+ {"source_address": "100.64.3.2", "public_address": "192.164.3.2", "source_port": 4500, "public_port": 6006},
+ {"source_address": "172.18.3.2", "public_address": "192.118.3.2", "source_port": 4500, "public_port": 6006},
+ ]
+ },
+ "expected": {
+ "result": "failure",
+ "messages": [
+ "For STUN source `100.64.3.2:4500`:\nExpected `192.164.3.2` as the public ip, but found `192.64.3.2` instead.",
+ "For STUN source `172.18.3.2:4500`:\nExpected `192.118.3.2` as the public ip, but found `192.18.3.2` instead.",
+ ],
+ },
+ },
+ {
+ "name": "failure-no-client",
+ "test": VerifyStunClient,
+ "eos_data": [
+ {"bindings": {}},
+ {"bindings": {}},
+ ],
+ "inputs": {
+ "stun_clients": [
+ {"source_address": "100.64.3.2", "public_address": "192.164.3.2", "source_port": 4500, "public_port": 6006},
+ {"source_address": "172.18.3.2", "public_address": "192.118.3.2", "source_port": 4500, "public_port": 6006},
+ ]
+ },
+ "expected": {
+ "result": "failure",
+ "messages": ["STUN client transaction for source `100.64.3.2:4500` is not found.", "STUN client transaction for source `172.18.3.2:4500` is not found."],
+ },
+ },
+ {
+ "name": "failure-incorrect-public-port",
+ "test": VerifyStunClient,
+ "eos_data": [
+ {"bindings": {}},
+ {
+ "bindings": {
+ "000000040a64ff0100000000": {
+ "sourceAddress": {"ip": "172.18.3.2", "port": 4500},
+ "publicAddress": {"ip": "192.18.3.2", "port": 4800},
+ }
+ }
+ },
+ ],
+ "inputs": {
+ "stun_clients": [
+ {"source_address": "100.64.3.2", "public_address": "192.164.3.2", "source_port": 4500, "public_port": 6006},
+ {"source_address": "172.18.3.2", "public_address": "192.118.3.2", "source_port": 4500, "public_port": 6006},
+ ]
+ },
+ "expected": {
+ "result": "failure",
+ "messages": [
+ "STUN client transaction for source `100.64.3.2:4500` is not found.",
+ "For STUN source `172.18.3.2:4500`:\n"
+ "Expected `192.118.3.2` as the public ip, but found `192.18.3.2` instead.\n"
+ "Expected `6006` as the public port, but found `4800` instead.",
+ ],
+ },
+ },
+ {
+ "name": "failure-all-type",
+ "test": VerifyStunClient,
+ "eos_data": [
+ {"bindings": {}},
+ {
+ "bindings": {
+ "000000040a64ff0100000000": {
+ "sourceAddress": {"ip": "172.18.3.2", "port": 4500},
+ "publicAddress": {"ip": "192.18.3.2", "port": 4800},
+ }
+ }
+ },
+ ],
+ "inputs": {
+ "stun_clients": [
+ {"source_address": "100.64.3.2", "public_address": "192.164.3.2", "source_port": 4500, "public_port": 6006},
+ {"source_address": "172.18.4.2", "public_address": "192.118.3.2", "source_port": 4800, "public_port": 6006},
+ ]
+ },
+ "expected": {
+ "result": "failure",
+ "messages": [
+ "STUN client transaction for source `100.64.3.2:4500` is not found.",
+ "For STUN source `172.18.4.2:4800`:\n"
+ "Expected `172.18.4.2` as the source ip, but found `172.18.3.2` instead.\n"
+ "Expected `4800` as the source port, but found `4500` instead.\n"
+ "Expected `192.118.3.2` as the public ip, but found `192.18.3.2` instead.\n"
+ "Expected `6006` as the public port, but found `4800` instead.",
+ ],
+ },
+ },
+]
diff --git a/tests/units/anta_tests/test_system.py b/tests/units/anta_tests/test_system.py
index 62260fa..6965461 100644
--- a/tests/units/anta_tests/test_system.py
+++ b/tests/units/anta_tests/test_system.py
@@ -1,7 +1,8 @@
# 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 inputs for anta.tests.system"""
+"""Test inputs for anta.tests.system."""
+
from __future__ import annotations
from typing import Any
@@ -46,10 +47,15 @@ DATA: list[dict[str, Any]] = [
"eos_data": [
{
"resetCauses": [
- {"recommendedAction": "No action necessary.", "description": "Reload requested by the user.", "timestamp": 1683186892.0, "debugInfoIsDir": False}
+ {
+ "recommendedAction": "No action necessary.",
+ "description": "Reload requested by the user.",
+ "timestamp": 1683186892.0,
+ "debugInfoIsDir": False,
+ },
],
"full": False,
- }
+ },
],
"inputs": None,
"expected": {"result": "success"},
@@ -61,10 +67,10 @@ DATA: list[dict[str, Any]] = [
"eos_data": [
{
"resetCauses": [
- {"recommendedAction": "No action necessary.", "description": "Reload after crash.", "timestamp": 1683186892.0, "debugInfoIsDir": False}
+ {"recommendedAction": "No action necessary.", "description": "Reload after crash.", "timestamp": 1683186892.0, "debugInfoIsDir": False},
],
"full": False,
- }
+ },
],
"inputs": None,
"expected": {"result": "failure", "messages": ["Reload cause is: 'Reload after crash.'"]},
@@ -125,7 +131,7 @@ EntityManager::doBackoff waiting for remote sysdb version ....ok
===> /var/log/agents/Acl-830 Fri Jul 7 15:07:00 2023 <===
===== Output from /usr/bin/Acl [] (PID=830) started Jul 7 15:06:10.871700 ===
EntityManager::doBackoff waiting for remote sysdb version ...................ok
-"""
+""",
],
"inputs": None,
"expected": {
@@ -158,9 +164,9 @@ EntityManager::doBackoff waiting for remote sysdb version ...................ok
"activeTime": 360,
"virtMem": "6644",
"sharedMem": "3996",
- }
+ },
},
- }
+ },
],
"inputs": None,
"expected": {"result": "success"},
@@ -185,9 +191,9 @@ EntityManager::doBackoff waiting for remote sysdb version ...................ok
"activeTime": 360,
"virtMem": "6644",
"sharedMem": "3996",
- }
+ },
},
- }
+ },
],
"inputs": None,
"expected": {"result": "failure", "messages": ["Device has reported a high CPU utilization: 75.2%"]},
@@ -203,7 +209,7 @@ EntityManager::doBackoff waiting for remote sysdb version ...................ok
"memTotal": 2004568,
"memFree": 879004,
"version": "4.27.3F",
- }
+ },
],
"inputs": None,
"expected": {"result": "success"},
@@ -219,7 +225,7 @@ EntityManager::doBackoff waiting for remote sysdb version ...................ok
"memTotal": 2004568,
"memFree": 89004,
"version": "4.27.3F",
- }
+ },
],
"inputs": None,
"expected": {"result": "failure", "messages": ["Device has reported a high memory usage: 95.56%"]},
@@ -233,7 +239,7 @@ EntityManager::doBackoff waiting for remote sysdb version ...................ok
none 294M 78M 217M 27% /
none 294M 78M 217M 27% /.overlay
/dev/loop0 461M 461M 0 100% /rootfs-i386
-"""
+""",
],
"inputs": None,
"expected": {"result": "success"},
@@ -247,7 +253,7 @@ none 294M 78M 217M 27% /.overlay
none 294M 78M 217M 27% /
none 294M 78M 217M 84% /.overlay
/dev/loop0 461M 461M 0 100% /rootfs-i386
-"""
+""",
],
"inputs": None,
"expected": {
@@ -264,7 +270,7 @@ none 294M 78M 217M 84% /.overlay
"eos_data": [
"""synchronised
poll interval unknown
-"""
+""",
],
"inputs": None,
"expected": {"result": "success"},
@@ -275,7 +281,7 @@ poll interval unknown
"eos_data": [
"""unsynchronised
poll interval unknown
-"""
+""",
],
"inputs": None,
"expected": {"result": "failure", "messages": ["The device is not synchronized with the configured NTP server(s): 'unsynchronised'"]},
diff --git a/tests/units/anta_tests/test_vlan.py b/tests/units/anta_tests/test_vlan.py
index 93398f6..53bf92f 100644
--- a/tests/units/anta_tests/test_vlan.py
+++ b/tests/units/anta_tests/test_vlan.py
@@ -1,9 +1,8 @@
# 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.
-"""
-Tests for anta.tests.vlan.py
-"""
+"""Tests for anta.tests.vlan.py."""
+
from __future__ import annotations
from typing import Any
diff --git a/tests/units/anta_tests/test_vxlan.py b/tests/units/anta_tests/test_vxlan.py
index 2a9a875..f450897 100644
--- a/tests/units/anta_tests/test_vxlan.py
+++ b/tests/units/anta_tests/test_vxlan.py
@@ -1,9 +1,8 @@
# 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.
-"""
-Tests for anta.tests.vxlan.py
-"""
+"""Tests for anta.tests.vxlan.py."""
+
from __future__ import annotations
from typing import Any
@@ -107,7 +106,7 @@ DATA: list[dict[str, Any]] = [
},
},
"warnings": [],
- }
+ },
],
"inputs": None,
"expected": {"result": "success"},
@@ -172,7 +171,7 @@ DATA: list[dict[str, Any]] = [
},
},
"warnings": ["Your configuration contains warnings. This does not mean misconfigurations. But you may wish to re-check your configurations."],
- }
+ },
],
"inputs": None,
"expected": {
@@ -184,7 +183,7 @@ DATA: list[dict[str, Any]] = [
"'No VLAN-VNI mapping in Vxlan1'}, {'name': 'Flood List', 'checkPass': False, 'hasWarning': True, 'detail': "
"'No VXLAN VLANs in Vxlan1'}, {'name': 'Routing', 'checkPass': True, 'hasWarning': False, 'detail': ''}, {'name': "
"'VNI VRF ACL', 'checkPass': True, 'hasWarning': False, 'detail': ''}, {'name': 'VRF-VNI Dynamic VLAN', 'checkPass': True, "
- "'hasWarning': False, 'detail': ''}, {'name': 'Decap VRF-VNI Map', 'checkPass': True, 'hasWarning': False, 'detail': ''}]}}"
+ "'hasWarning': False, 'detail': ''}, {'name': 'Decap VRF-VNI Map', 'checkPass': True, 'hasWarning': False, 'detail': ''}]}}",
],
},
},
@@ -203,12 +202,12 @@ DATA: list[dict[str, Any]] = [
"vxlanIntfs": {
"Vxlan1": {
"vniBindings": {
- "10020": {"vlan": 20, "dynamicVlan": False, "source": "static", "interfaces": {"Ethernet31": {"dot1q": 0}, "Vxlan1": {"dot1q": 20}}}
+ "10020": {"vlan": 20, "dynamicVlan": False, "source": "static", "interfaces": {"Ethernet31": {"dot1q": 0}, "Vxlan1": {"dot1q": 20}}},
},
"vniBindingsToVrf": {"500": {"vrfName": "PROD", "vlan": 1199, "source": "evpn"}},
- }
- }
- }
+ },
+ },
+ },
],
"inputs": {"bindings": {10020: 20, 500: 1199}},
"expected": {"result": "success"},
@@ -221,12 +220,12 @@ DATA: list[dict[str, Any]] = [
"vxlanIntfs": {
"Vxlan1": {
"vniBindings": {
- "10020": {"vlan": 20, "dynamicVlan": False, "source": "static", "interfaces": {"Ethernet31": {"dot1q": 0}, "Vxlan1": {"dot1q": 20}}}
+ "10020": {"vlan": 20, "dynamicVlan": False, "source": "static", "interfaces": {"Ethernet31": {"dot1q": 0}, "Vxlan1": {"dot1q": 20}}},
},
"vniBindingsToVrf": {"500": {"vrfName": "PROD", "vlan": 1199, "source": "evpn"}},
- }
- }
- }
+ },
+ },
+ },
],
"inputs": {"bindings": {10010: 10, 10020: 20, 500: 1199}},
"expected": {"result": "failure", "messages": ["The following VNI(s) have no binding: ['10010']"]},
@@ -239,12 +238,12 @@ DATA: list[dict[str, Any]] = [
"vxlanIntfs": {
"Vxlan1": {
"vniBindings": {
- "10020": {"vlan": 30, "dynamicVlan": False, "source": "static", "interfaces": {"Ethernet31": {"dot1q": 0}, "Vxlan1": {"dot1q": 20}}}
+ "10020": {"vlan": 30, "dynamicVlan": False, "source": "static", "interfaces": {"Ethernet31": {"dot1q": 0}, "Vxlan1": {"dot1q": 20}}},
},
"vniBindingsToVrf": {"500": {"vrfName": "PROD", "vlan": 1199, "source": "evpn"}},
- }
- }
- }
+ },
+ },
+ },
],
"inputs": {"bindings": {10020: 20, 500: 1199}},
"expected": {"result": "failure", "messages": ["The following VNI(s) have the wrong VLAN binding: [{'10020': 30}]"]},
@@ -257,12 +256,12 @@ DATA: list[dict[str, Any]] = [
"vxlanIntfs": {
"Vxlan1": {
"vniBindings": {
- "10020": {"vlan": 30, "dynamicVlan": False, "source": "static", "interfaces": {"Ethernet31": {"dot1q": 0}, "Vxlan1": {"dot1q": 20}}}
+ "10020": {"vlan": 30, "dynamicVlan": False, "source": "static", "interfaces": {"Ethernet31": {"dot1q": 0}, "Vxlan1": {"dot1q": 20}}},
},
"vniBindingsToVrf": {"500": {"vrfName": "PROD", "vlan": 1199, "source": "evpn"}},
- }
- }
- }
+ },
+ },
+ },
],
"inputs": {"bindings": {10010: 10, 10020: 20, 500: 1199}},
"expected": {
diff --git a/tests/units/cli/__init__.py b/tests/units/cli/__init__.py
index e772bee..1d4cf6c 100644
--- a/tests/units/cli/__init__.py
+++ b/tests/units/cli/__init__.py
@@ -1,3 +1,4 @@
# 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.cli submodule."""
diff --git a/tests/units/cli/check/__init__.py b/tests/units/cli/check/__init__.py
index e772bee..a116af4 100644
--- a/tests/units/cli/check/__init__.py
+++ b/tests/units/cli/check/__init__.py
@@ -1,3 +1,4 @@
# 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.cli.check submodule."""
diff --git a/tests/units/cli/check/test__init__.py b/tests/units/cli/check/test__init__.py
index a3a770b..2501dc8 100644
--- a/tests/units/cli/check/test__init__.py
+++ b/tests/units/cli/check/test__init__.py
@@ -1,30 +1,28 @@
# 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.
-"""
-Tests for anta.cli.check
-"""
+"""Tests for anta.cli.check."""
+
from __future__ import annotations
-from click.testing import CliRunner
+from typing import TYPE_CHECKING
from anta.cli import anta
from anta.cli.utils import ExitCode
+if TYPE_CHECKING:
+ from click.testing import CliRunner
+
def test_anta_check(click_runner: CliRunner) -> None:
- """
- Test anta check
- """
+ """Test anta check."""
result = click_runner.invoke(anta, ["check"])
assert result.exit_code == ExitCode.OK
assert "Usage: anta check" in result.output
def test_anta_check_help(click_runner: CliRunner) -> None:
- """
- Test anta check --help
- """
+ """Test anta check --help."""
result = click_runner.invoke(anta, ["check", "--help"])
assert result.exit_code == ExitCode.OK
assert "Usage: anta check" in result.output
diff --git a/tests/units/cli/check/test_commands.py b/tests/units/cli/check/test_commands.py
index 746b315..11c2b5f 100644
--- a/tests/units/cli/check/test_commands.py
+++ b/tests/units/cli/check/test_commands.py
@@ -1,9 +1,8 @@
# 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.
-"""
-Tests for anta.cli.check.commands
-"""
+"""Tests for anta.cli.check.commands."""
+
from __future__ import annotations
from pathlib import Path
@@ -21,7 +20,7 @@ DATA_DIR: Path = Path(__file__).parents[3].resolve() / "data"
@pytest.mark.parametrize(
- "catalog_path, expected_exit, expected_output",
+ ("catalog_path", "expected_exit", "expected_output"),
[
pytest.param("ghost_catalog.yml", ExitCode.USAGE_ERROR, "Error: Invalid value for '--catalog'", id="catalog does not exist"),
pytest.param("test_catalog_with_undefined_module.yml", ExitCode.USAGE_ERROR, "Test catalog is invalid!", id="catalog is not valid"),
@@ -29,9 +28,7 @@ DATA_DIR: Path = Path(__file__).parents[3].resolve() / "data"
],
)
def test_catalog(click_runner: CliRunner, catalog_path: Path, expected_exit: int, expected_output: str) -> None:
- """
- Test `anta check catalog -c catalog
- """
+ """Test `anta check catalog -c catalog."""
result = click_runner.invoke(anta, ["check", "catalog", "-c", str(DATA_DIR / catalog_path)])
assert result.exit_code == expected_exit
assert expected_output in result.output
diff --git a/tests/units/cli/debug/__init__.py b/tests/units/cli/debug/__init__.py
index e772bee..ccce49c 100644
--- a/tests/units/cli/debug/__init__.py
+++ b/tests/units/cli/debug/__init__.py
@@ -1,3 +1,4 @@
# 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.cli.debug submodule."""
diff --git a/tests/units/cli/debug/test__init__.py b/tests/units/cli/debug/test__init__.py
index 062182d..fd3663f 100644
--- a/tests/units/cli/debug/test__init__.py
+++ b/tests/units/cli/debug/test__init__.py
@@ -1,30 +1,28 @@
# 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.
-"""
-Tests for anta.cli.debug
-"""
+"""Tests for anta.cli.debug."""
+
from __future__ import annotations
-from click.testing import CliRunner
+from typing import TYPE_CHECKING
from anta.cli import anta
from anta.cli.utils import ExitCode
+if TYPE_CHECKING:
+ from click.testing import CliRunner
+
def test_anta_debug(click_runner: CliRunner) -> None:
- """
- Test anta debug
- """
+ """Test anta debug."""
result = click_runner.invoke(anta, ["debug"])
assert result.exit_code == ExitCode.OK
assert "Usage: anta debug" in result.output
def test_anta_debug_help(click_runner: CliRunner) -> None:
- """
- Test anta debug --help
- """
+ """Test anta debug --help."""
result = click_runner.invoke(anta, ["debug", "--help"])
assert result.exit_code == ExitCode.OK
assert "Usage: anta debug" in result.output
diff --git a/tests/units/cli/debug/test_commands.py b/tests/units/cli/debug/test_commands.py
index 6d9ac29..76c3648 100644
--- a/tests/units/cli/debug/test_commands.py
+++ b/tests/units/cli/debug/test_commands.py
@@ -1,9 +1,8 @@
# 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.
-"""
-Tests for anta.cli.debug.commands
-"""
+"""Tests for anta.cli.debug.commands."""
+
from __future__ import annotations
from typing import TYPE_CHECKING, Literal
@@ -18,7 +17,7 @@ if TYPE_CHECKING:
@pytest.mark.parametrize(
- "command, ofmt, version, revision, device, failed",
+ ("command", "ofmt", "version", "revision", "device", "failed"),
[
pytest.param("show version", "json", None, None, "dummy", False, id="json command"),
pytest.param("show version", "text", None, None, "dummy", False, id="text command"),
@@ -29,11 +28,15 @@ if TYPE_CHECKING:
],
)
def test_run_cmd(
- click_runner: CliRunner, command: str, ofmt: Literal["json", "text"], version: Literal["1", "latest"] | None, revision: int | None, device: str, failed: bool
+ click_runner: CliRunner,
+ command: str,
+ ofmt: Literal["json", "text"],
+ version: Literal["1", "latest"] | None,
+ revision: int | None,
+ device: str,
+ failed: bool,
) -> None:
- """
- Test `anta debug run-cmd`
- """
+ """Test `anta debug run-cmd`."""
# pylint: disable=too-many-arguments
cli_args = ["-l", "debug", "debug", "run-cmd", "--command", command, "--device", device]
diff --git a/tests/units/cli/exec/__init__.py b/tests/units/cli/exec/__init__.py
index e772bee..4ed48bc 100644
--- a/tests/units/cli/exec/__init__.py
+++ b/tests/units/cli/exec/__init__.py
@@ -1,3 +1,4 @@
# 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.cli.exec submodule."""
diff --git a/tests/units/cli/exec/test__init__.py b/tests/units/cli/exec/test__init__.py
index f8ad365..124d4af 100644
--- a/tests/units/cli/exec/test__init__.py
+++ b/tests/units/cli/exec/test__init__.py
@@ -1,30 +1,28 @@
# 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.
-"""
-Tests for anta.cli.exec
-"""
+"""Tests for anta.cli.exec."""
+
from __future__ import annotations
-from click.testing import CliRunner
+from typing import TYPE_CHECKING
from anta.cli import anta
from anta.cli.utils import ExitCode
+if TYPE_CHECKING:
+ from click.testing import CliRunner
+
def test_anta_exec(click_runner: CliRunner) -> None:
- """
- Test anta exec
- """
+ """Test anta exec."""
result = click_runner.invoke(anta, ["exec"])
assert result.exit_code == ExitCode.OK
assert "Usage: anta exec" in result.output
def test_anta_exec_help(click_runner: CliRunner) -> None:
- """
- Test anta exec --help
- """
+ """Test anta exec --help."""
result = click_runner.invoke(anta, ["exec", "--help"])
assert result.exit_code == ExitCode.OK
assert "Usage: anta exec" in result.output
diff --git a/tests/units/cli/exec/test_commands.py b/tests/units/cli/exec/test_commands.py
index f96d7f6..4a72d63 100644
--- a/tests/units/cli/exec/test_commands.py
+++ b/tests/units/cli/exec/test_commands.py
@@ -1,9 +1,7 @@
# 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.
-"""
-Tests for anta.cli.exec.commands
-"""
+"""Tests for anta.cli.exec.commands."""
from __future__ import annotations
@@ -21,27 +19,21 @@ if TYPE_CHECKING:
def test_clear_counters_help(click_runner: CliRunner) -> None:
- """
- Test `anta exec clear-counters --help`
- """
+ """Test `anta exec clear-counters --help`."""
result = click_runner.invoke(clear_counters, ["--help"])
assert result.exit_code == 0
assert "Usage" in result.output
def test_snapshot_help(click_runner: CliRunner) -> None:
- """
- Test `anta exec snapshot --help`
- """
+ """Test `anta exec snapshot --help`."""
result = click_runner.invoke(snapshot, ["--help"])
assert result.exit_code == 0
assert "Usage" in result.output
def test_collect_tech_support_help(click_runner: CliRunner) -> None:
- """
- Test `anta exec collect-tech-support --help`
- """
+ """Test `anta exec collect-tech-support --help`."""
result = click_runner.invoke(collect_tech_support, ["--help"])
assert result.exit_code == 0
assert "Usage" in result.output
@@ -55,9 +47,7 @@ def test_collect_tech_support_help(click_runner: CliRunner) -> None:
],
)
def test_clear_counters(click_runner: CliRunner, tags: str | None) -> None:
- """
- Test `anta exec clear-counters`
- """
+ """Test `anta exec clear-counters`."""
cli_args = ["exec", "clear-counters"]
if tags is not None:
cli_args.extend(["--tags", tags])
@@ -69,7 +59,7 @@ COMMAND_LIST_PATH_FILE = Path(__file__).parent.parent.parent.parent / "data" / "
@pytest.mark.parametrize(
- "commands_path, tags",
+ ("commands_path", "tags"),
[
pytest.param(None, None, id="missing command list"),
pytest.param(Path("/I/do/not/exist"), None, id="wrong path for command_list"),
@@ -78,9 +68,7 @@ COMMAND_LIST_PATH_FILE = Path(__file__).parent.parent.parent.parent / "data" / "
],
)
def test_snapshot(tmp_path: Path, click_runner: CliRunner, commands_path: Path | None, tags: str | None) -> None:
- """
- Test `anta exec snapshot`
- """
+ """Test `anta exec snapshot`."""
cli_args = ["exec", "snapshot", "--output", str(tmp_path)]
# Need to mock datetetime
if commands_path is not None:
@@ -99,7 +87,7 @@ def test_snapshot(tmp_path: Path, click_runner: CliRunner, commands_path: Path |
@pytest.mark.parametrize(
- "output, latest, configure, tags",
+ ("output", "latest", "configure", "tags"),
[
pytest.param(None, None, False, None, id="no params"),
pytest.param("/tmp/dummy", None, False, None, id="with output"),
@@ -109,9 +97,7 @@ def test_snapshot(tmp_path: Path, click_runner: CliRunner, commands_path: Path |
],
)
def test_collect_tech_support(click_runner: CliRunner, output: str | None, latest: str | None, configure: bool | None, tags: str | None) -> None:
- """
- Test `anta exec collect-tech-support`
- """
+ """Test `anta exec collect-tech-support`."""
cli_args = ["exec", "collect-tech-support"]
if output is not None:
cli_args.extend(["--output", output])
diff --git a/tests/units/cli/exec/test_utils.py b/tests/units/cli/exec/test_utils.py
index 6df1c86..455568b 100644
--- a/tests/units/cli/exec/test_utils.py
+++ b/tests/units/cli/exec/test_utils.py
@@ -1,9 +1,7 @@
# 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.
-"""
-Tests for anta.cli.exec.utils
-"""
+"""Tests for anta.cli.exec.utils."""
from __future__ import annotations
@@ -12,40 +10,59 @@ from unittest.mock import call, patch
import pytest
-from anta.cli.exec.utils import clear_counters_utils # , collect_commands, collect_scheduled_show_tech
-from anta.device import AntaDevice
-from anta.inventory import AntaInventory
+from anta.cli.exec.utils import (
+ clear_counters_utils,
+)
from anta.models import AntaCommand
+# , collect_commands, collect_scheduled_show_tech
+
if TYPE_CHECKING:
- from pytest import LogCaptureFixture
+ from anta.device import AntaDevice
+ from anta.inventory import AntaInventory
-# TODO complete test cases
-@pytest.mark.asyncio
+# TODO: complete test cases
+@pytest.mark.asyncio()
@pytest.mark.parametrize(
- "inventory_state, per_device_command_output, tags",
+ ("inventory_state", "per_device_command_output", "tags"),
[
pytest.param(
- {"dummy": {"is_online": False}, "dummy2": {"is_online": False}, "dummy3": {"is_online": False}},
+ {
+ "dummy": {"is_online": False},
+ "dummy2": {"is_online": False},
+ "dummy3": {"is_online": False},
+ },
{},
None,
id="no_connected_device",
),
pytest.param(
- {"dummy": {"is_online": True, "hw_model": "cEOSLab"}, "dummy2": {"is_online": True, "hw_model": "vEOS-lab"}, "dummy3": {"is_online": False}},
+ {
+ "dummy": {"is_online": True, "hw_model": "cEOSLab"},
+ "dummy2": {"is_online": True, "hw_model": "vEOS-lab"},
+ "dummy3": {"is_online": False},
+ },
{},
None,
id="cEOSLab and vEOS-lab devices",
),
pytest.param(
- {"dummy": {"is_online": True}, "dummy2": {"is_online": True}, "dummy3": {"is_online": False}},
+ {
+ "dummy": {"is_online": True},
+ "dummy2": {"is_online": True},
+ "dummy3": {"is_online": False},
+ },
{"dummy": None}, # None means the command failed to collect
None,
id="device with error",
),
pytest.param(
- {"dummy": {"is_online": True}, "dummy2": {"is_online": True}, "dummy3": {"is_online": True}},
+ {
+ "dummy": {"is_online": True},
+ "dummy2": {"is_online": True},
+ "dummy3": {"is_online": True},
+ },
{},
["spine"],
id="tags",
@@ -53,42 +70,38 @@ if TYPE_CHECKING:
],
)
async def test_clear_counters_utils(
- caplog: LogCaptureFixture,
+ caplog: pytest.LogCaptureFixture,
test_inventory: AntaInventory,
inventory_state: dict[str, Any],
per_device_command_output: dict[str, Any],
- tags: list[str] | None,
+ tags: set[str] | None,
) -> None:
- """
- Test anta.cli.exec.utils.clear_counters_utils
- """
+ """Test anta.cli.exec.utils.clear_counters_utils."""
async def mock_connect_inventory() -> None:
- """
- mocking connect_inventory coroutine
- """
+ """Mock connect_inventory coroutine."""
for name, device in test_inventory.items():
device.is_online = inventory_state[name].get("is_online", True)
device.established = inventory_state[name].get("established", device.is_online)
device.hw_model = inventory_state[name].get("hw_model", "dummy")
async def dummy_collect(self: AntaDevice, command: AntaCommand) -> None:
- """
- mocking collect coroutine
- """
+ """Mock collect coroutine."""
command.output = per_device_command_output.get(self.name, "")
# Need to patch the child device class
- with patch("anta.device.AsyncEOSDevice.collect", side_effect=dummy_collect, autospec=True) as mocked_collect, patch(
- "anta.inventory.AntaInventory.connect_inventory",
- side_effect=mock_connect_inventory,
- ) as mocked_connect_inventory:
- print(mocked_collect)
+ with (
+ patch("anta.device.AsyncEOSDevice.collect", side_effect=dummy_collect, autospec=True) as mocked_collect,
+ patch(
+ "anta.inventory.AntaInventory.connect_inventory",
+ side_effect=mock_connect_inventory,
+ ) as mocked_connect_inventory,
+ ):
mocked_collect.side_effect = dummy_collect
await clear_counters_utils(test_inventory, tags=tags)
mocked_connect_inventory.assert_awaited_once()
- devices_established = list(test_inventory.get_inventory(established_only=True, tags=tags).values())
+ devices_established = test_inventory.get_inventory(established_only=True, tags=tags).devices
if devices_established:
# Building the list of calls
calls = []
@@ -96,32 +109,28 @@ async def test_clear_counters_utils(
calls.append(
call(
device,
- **{
- "command": AntaCommand(
- command="clear counters",
- version="latest",
- revision=None,
- ofmt="json",
- output=per_device_command_output.get(device.name, ""),
- errors=[],
- )
- },
- )
+ command=AntaCommand(
+ command="clear counters",
+ version="latest",
+ revision=None,
+ ofmt="json",
+ output=per_device_command_output.get(device.name, ""),
+ errors=[],
+ ),
+ ),
)
if device.hw_model not in ["cEOSLab", "vEOS-lab"]:
calls.append(
call(
device,
- **{
- "command": AntaCommand(
- command="clear hardware counter drop",
- version="latest",
- revision=None,
- ofmt="json",
- output=per_device_command_output.get(device.name, ""),
- )
- },
- )
+ command=AntaCommand(
+ command="clear hardware counter drop",
+ version="latest",
+ revision=None,
+ ofmt="json",
+ output=per_device_command_output.get(device.name, ""),
+ ),
+ ),
)
mocked_collect.assert_has_awaits(calls)
# Check error
diff --git a/tests/units/cli/get/__init__.py b/tests/units/cli/get/__init__.py
index e772bee..5517ded 100644
--- a/tests/units/cli/get/__init__.py
+++ b/tests/units/cli/get/__init__.py
@@ -1,3 +1,4 @@
# 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.cli.get submodule."""
diff --git a/tests/units/cli/get/test__init__.py b/tests/units/cli/get/test__init__.py
index b18ef88..a6a0c3c 100644
--- a/tests/units/cli/get/test__init__.py
+++ b/tests/units/cli/get/test__init__.py
@@ -1,30 +1,28 @@
# 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.
-"""
-Tests for anta.cli.get
-"""
+"""Tests for anta.cli.get."""
+
from __future__ import annotations
-from click.testing import CliRunner
+from typing import TYPE_CHECKING
from anta.cli import anta
from anta.cli.utils import ExitCode
+if TYPE_CHECKING:
+ from click.testing import CliRunner
+
def test_anta_get(click_runner: CliRunner) -> None:
- """
- Test anta get
- """
+ """Test anta get."""
result = click_runner.invoke(anta, ["get"])
assert result.exit_code == ExitCode.OK
assert "Usage: anta get" in result.output
def test_anta_get_help(click_runner: CliRunner) -> None:
- """
- Test anta get --help
- """
+ """Test anta get --help."""
result = click_runner.invoke(anta, ["get", "--help"])
assert result.exit_code == ExitCode.OK
assert "Usage: anta get" in result.output
diff --git a/tests/units/cli/get/test_commands.py b/tests/units/cli/get/test_commands.py
index aa6dc4f..9edc7c3 100644
--- a/tests/units/cli/get/test_commands.py
+++ b/tests/units/cli/get/test_commands.py
@@ -1,9 +1,8 @@
# 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.
-"""
-Tests for anta.cli.get.commands
-"""
+"""Tests for anta.cli.get.commands."""
+
from __future__ import annotations
import filecmp
@@ -12,7 +11,6 @@ from typing import TYPE_CHECKING
from unittest.mock import ANY, patch
import pytest
-from cvprac.cvp_client import CvpClient
from cvprac.cvp_client_errors import CvpApiError
from anta.cli import anta
@@ -20,12 +18,13 @@ from anta.cli.utils import ExitCode
if TYPE_CHECKING:
from click.testing import CliRunner
+ from cvprac.cvp_client import CvpClient
DATA_DIR: Path = Path(__file__).parents[3].resolve() / "data"
@pytest.mark.parametrize(
- "cvp_container, cvp_connect_failure",
+ ("cvp_container", "cvp_connect_failure"),
[
pytest.param(None, False, id="all devices"),
pytest.param("custom_container", False, id="custom container"),
@@ -38,28 +37,46 @@ def test_from_cvp(
cvp_container: str | None,
cvp_connect_failure: bool,
) -> None:
- """
- Test `anta get from-cvp`
+ """Test `anta get from-cvp`.
This test verifies that username and password are NOT mandatory to run this command
"""
output: Path = tmp_path / "output.yml"
- cli_args = ["get", "from-cvp", "--output", str(output), "--host", "42.42.42.42", "--username", "anta", "--password", "anta"]
+ cli_args = [
+ "get",
+ "from-cvp",
+ "--output",
+ str(output),
+ "--host",
+ "42.42.42.42",
+ "--username",
+ "anta",
+ "--password",
+ "anta",
+ ]
if cvp_container is not None:
cli_args.extend(["--container", cvp_container])
- def mock_cvp_connect(self: CvpClient, *args: str, **kwargs: str) -> None:
- # pylint: disable=unused-argument
+ def mock_cvp_connect(_self: CvpClient, *_args: str, **_kwargs: str) -> None:
if cvp_connect_failure:
raise CvpApiError(msg="mocked CvpApiError")
# always get a token
- with patch("anta.cli.get.commands.get_cv_token", return_value="dummy_token"), patch(
- "cvprac.cvp_client.CvpClient.connect", autospec=True, side_effect=mock_cvp_connect
- ) as mocked_cvp_connect, patch("cvprac.cvp_client.CvpApi.get_inventory", autospec=True, return_value=[]) as mocked_get_inventory, patch(
- "cvprac.cvp_client.CvpApi.get_devices_in_container", autospec=True, return_value=[]
- ) as mocked_get_devices_in_container:
+ with (
+ patch("anta.cli.get.commands.get_cv_token", return_value="dummy_token"),
+ patch(
+ "cvprac.cvp_client.CvpClient.connect",
+ autospec=True,
+ side_effect=mock_cvp_connect,
+ ) as mocked_cvp_connect,
+ patch("cvprac.cvp_client.CvpApi.get_inventory", autospec=True, return_value=[]) as mocked_get_inventory,
+ patch(
+ "cvprac.cvp_client.CvpApi.get_devices_in_container",
+ autospec=True,
+ return_value=[],
+ ) as mocked_get_devices_in_container,
+ ):
result = click_runner.invoke(anta, cli_args)
if not cvp_connect_failure:
@@ -79,12 +96,24 @@ def test_from_cvp(
@pytest.mark.parametrize(
- "ansible_inventory, ansible_group, expected_exit, expected_log",
+ ("ansible_inventory", "ansible_group", "expected_exit", "expected_log"),
[
pytest.param("ansible_inventory.yml", None, ExitCode.OK, None, id="no group"),
pytest.param("ansible_inventory.yml", "ATD_LEAFS", ExitCode.OK, None, id="group found"),
- pytest.param("ansible_inventory.yml", "DUMMY", ExitCode.USAGE_ERROR, "Group DUMMY not found in Ansible inventory", id="group not found"),
- pytest.param("empty_ansible_inventory.yml", None, ExitCode.USAGE_ERROR, "is empty", id="empty inventory"),
+ pytest.param(
+ "ansible_inventory.yml",
+ "DUMMY",
+ ExitCode.USAGE_ERROR,
+ "Group DUMMY not found in Ansible inventory",
+ id="group not found",
+ ),
+ pytest.param(
+ "empty_ansible_inventory.yml",
+ None,
+ ExitCode.USAGE_ERROR,
+ "is empty",
+ id="empty inventory",
+ ),
],
)
def test_from_ansible(
@@ -95,8 +124,8 @@ def test_from_ansible(
expected_exit: int,
expected_log: str | None,
) -> None:
- """
- Test `anta get from-ansible`
+ # pylint: disable=too-many-arguments
+ """Test `anta get from-ansible`.
This test verifies:
* the parsing of an ansible-inventory
@@ -107,7 +136,14 @@ def test_from_ansible(
output: Path = tmp_path / "output.yml"
ansible_inventory_path = DATA_DIR / ansible_inventory
# Init cli_args
- cli_args = ["get", "from-ansible", "--output", str(output), "--ansible-inventory", str(ansible_inventory_path)]
+ cli_args = [
+ "get",
+ "from-ansible",
+ "--output",
+ str(output),
+ "--ansible-inventory",
+ str(ansible_inventory_path),
+ ]
# Set --ansible-group
if ansible_group is not None:
@@ -122,14 +158,30 @@ def test_from_ansible(
assert expected_log in result.output
else:
assert output.exists()
- # TODO check size of generated inventory to validate the group functionality!
+ # TODO: check size of generated inventory to validate the group functionality!
@pytest.mark.parametrize(
- "env_set, overwrite, is_tty, prompt, expected_exit, expected_log",
+ ("env_set", "overwrite", "is_tty", "prompt", "expected_exit", "expected_log"),
[
- pytest.param(True, False, True, "y", ExitCode.OK, "", id="no-overwrite-tty-init-prompt-yes"),
- pytest.param(True, False, True, "N", ExitCode.INTERNAL_ERROR, "Aborted", id="no-overwrite-tty-init-prompt-no"),
+ pytest.param(
+ True,
+ False,
+ True,
+ "y",
+ ExitCode.OK,
+ "",
+ id="no-overwrite-tty-init-prompt-yes",
+ ),
+ pytest.param(
+ True,
+ False,
+ True,
+ "N",
+ ExitCode.INTERNAL_ERROR,
+ "Aborted",
+ id="no-overwrite-tty-init-prompt-no",
+ ),
pytest.param(
True,
False,
@@ -159,8 +211,7 @@ def test_from_ansible_overwrite(
expected_log: str | None,
) -> None:
# pylint: disable=too-many-arguments
- """
- Test `anta get from-ansible` overwrite mechanism
+ """Test `anta get from-ansible` overwrite mechanism.
The test uses a static ansible-inventory and output as these are tested in other functions
@@ -177,7 +228,12 @@ def test_from_ansible_overwrite(
ansible_inventory_path = DATA_DIR / "ansible_inventory.yml"
expected_anta_inventory_path = DATA_DIR / "expected_anta_inventory.yml"
tmp_output = tmp_path / "output.yml"
- cli_args = ["get", "from-ansible", "--ansible-inventory", str(ansible_inventory_path)]
+ cli_args = [
+ "get",
+ "from-ansible",
+ "--ansible-inventory",
+ str(ansible_inventory_path),
+ ]
if env_set:
tmp_inv = Path(str(temp_env["ANTA_INVENTORY"]))
diff --git a/tests/units/cli/get/test_utils.py b/tests/units/cli/get/test_utils.py
index b335880..0dce335 100644
--- a/tests/units/cli/get/test_utils.py
+++ b/tests/units/cli/get/test_utils.py
@@ -1,12 +1,11 @@
# 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.
-"""
-Tests for anta.cli.get.utils
-"""
+"""Tests for anta.cli.get.utils."""
+
from __future__ import annotations
-from contextlib import nullcontext
+from contextlib import AbstractContextManager, nullcontext
from pathlib import Path
from typing import Any
from unittest.mock import MagicMock, patch
@@ -21,10 +20,8 @@ DATA_DIR: Path = Path(__file__).parents[3].resolve() / "data"
def test_get_cv_token() -> None:
- """
- Test anta.get.utils.get_cv_token
- """
- ip = "42.42.42.42"
+ """Test anta.get.utils.get_cv_token."""
+ ip_addr = "42.42.42.42"
username = "ant"
password = "formica"
@@ -32,7 +29,7 @@ def test_get_cv_token() -> None:
mocked_ret = MagicMock(autospec=requests.Response)
mocked_ret.json.return_value = {"sessionId": "simple"}
patched_request.return_value = mocked_ret
- res = get_cv_token(ip, username, password)
+ res = get_cv_token(ip_addr, username, password)
patched_request.assert_called_once_with(
"POST",
"https://42.42.42.42/cvpservice/login/authenticate.do",
@@ -72,9 +69,7 @@ CVP_INVENTORY = [
],
)
def test_create_inventory_from_cvp(tmp_path: Path, inventory: list[dict[str, Any]]) -> None:
- """
- Test anta.get.utils.create_inventory_from_cvp
- """
+ """Test anta.get.utils.create_inventory_from_cvp."""
output = tmp_path / "output.yml"
create_inventory_from_cvp(inventory, output)
@@ -86,19 +81,41 @@ def test_create_inventory_from_cvp(tmp_path: Path, inventory: list[dict[str, Any
@pytest.mark.parametrize(
- "inventory_filename, ansible_group, expected_raise, expected_inv_length",
+ ("inventory_filename", "ansible_group", "expected_raise", "expected_inv_length"),
[
pytest.param("ansible_inventory.yml", None, nullcontext(), 7, id="no group"),
pytest.param("ansible_inventory.yml", "ATD_LEAFS", nullcontext(), 4, id="group found"),
- pytest.param("ansible_inventory.yml", "DUMMY", pytest.raises(ValueError, match="Group DUMMY not found in Ansible inventory"), 0, id="group not found"),
- pytest.param("empty_ansible_inventory.yml", None, pytest.raises(ValueError, match="Ansible inventory .* is empty"), 0, id="empty inventory"),
- pytest.param("wrong_ansible_inventory.yml", None, pytest.raises(ValueError, match="Could not parse"), 0, id="os error inventory"),
+ pytest.param(
+ "ansible_inventory.yml",
+ "DUMMY",
+ pytest.raises(ValueError, match="Group DUMMY not found in Ansible inventory"),
+ 0,
+ id="group not found",
+ ),
+ pytest.param(
+ "empty_ansible_inventory.yml",
+ None,
+ pytest.raises(ValueError, match="Ansible inventory .* is empty"),
+ 0,
+ id="empty inventory",
+ ),
+ pytest.param(
+ "wrong_ansible_inventory.yml",
+ None,
+ pytest.raises(ValueError, match="Could not parse"),
+ 0,
+ id="os error inventory",
+ ),
],
)
-def test_create_inventory_from_ansible(tmp_path: Path, inventory_filename: Path, ansible_group: str | None, expected_raise: Any, expected_inv_length: int) -> None:
- """
- Test anta.get.utils.create_inventory_from_ansible
- """
+def test_create_inventory_from_ansible(
+ tmp_path: Path,
+ inventory_filename: Path,
+ ansible_group: str | None,
+ expected_raise: AbstractContextManager[Exception],
+ expected_inv_length: int,
+) -> None:
+ """Test anta.get.utils.create_inventory_from_ansible."""
target_file = tmp_path / "inventory.yml"
inventory_file_path = DATA_DIR / inventory_filename
diff --git a/tests/units/cli/nrfu/__init__.py b/tests/units/cli/nrfu/__init__.py
index e772bee..db71b4d 100644
--- a/tests/units/cli/nrfu/__init__.py
+++ b/tests/units/cli/nrfu/__init__.py
@@ -1,3 +1,4 @@
# 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.cli.nrfu submodule."""
diff --git a/tests/units/cli/nrfu/test__init__.py b/tests/units/cli/nrfu/test__init__.py
index fea641c..052c7c3 100644
--- a/tests/units/cli/nrfu/test__init__.py
+++ b/tests/units/cli/nrfu/test__init__.py
@@ -1,33 +1,31 @@
# 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.
-"""
-Tests for anta.cli.nrfu
-"""
+"""Tests for anta.cli.nrfu."""
+
from __future__ import annotations
-from click.testing import CliRunner
+from typing import TYPE_CHECKING
from anta.cli import anta
from anta.cli.utils import ExitCode
from tests.lib.utils import default_anta_env
+if TYPE_CHECKING:
+ from click.testing import CliRunner
+
# TODO: write unit tests for ignore-status and ignore-error
def test_anta_nrfu_help(click_runner: CliRunner) -> None:
- """
- Test anta nrfu --help
- """
+ """Test anta nrfu --help."""
result = click_runner.invoke(anta, ["nrfu", "--help"])
assert result.exit_code == ExitCode.OK
assert "Usage: anta nrfu" in result.output
def test_anta_nrfu(click_runner: CliRunner) -> None:
- """
- Test anta nrfu, catalog is given via env
- """
+ """Test anta nrfu, catalog is given via env."""
result = click_runner.invoke(anta, ["nrfu"])
assert result.exit_code == ExitCode.OK
assert "ANTA Inventory contains 3 devices" in result.output
@@ -35,9 +33,7 @@ def test_anta_nrfu(click_runner: CliRunner) -> None:
def test_anta_password_required(click_runner: CliRunner) -> None:
- """
- Test that password is provided
- """
+ """Test that password is provided."""
env = default_anta_env()
env["ANTA_PASSWORD"] = None
result = click_runner.invoke(anta, ["nrfu"], env=env)
@@ -47,9 +43,7 @@ def test_anta_password_required(click_runner: CliRunner) -> None:
def test_anta_password(click_runner: CliRunner) -> None:
- """
- Test that password can be provided either via --password or --prompt
- """
+ """Test that password can be provided either via --password or --prompt."""
env = default_anta_env()
env["ANTA_PASSWORD"] = None
result = click_runner.invoke(anta, ["nrfu", "--password", "secret"], env=env)
@@ -59,9 +53,7 @@ def test_anta_password(click_runner: CliRunner) -> None:
def test_anta_enable_password(click_runner: CliRunner) -> None:
- """
- Test that enable password can be provided either via --enable-password or --prompt
- """
+ """Test that enable password can be provided either via --enable-password or --prompt."""
# Both enable and enable-password
result = click_runner.invoke(anta, ["nrfu", "--enable", "--enable-password", "secret"])
assert result.exit_code == ExitCode.OK
@@ -78,7 +70,6 @@ def test_anta_enable_password(click_runner: CliRunner) -> None:
assert "Please enter a password to enter EOS privileged EXEC mode" not in result.output
assert result.exit_code == ExitCode.OK
- # enable and enable-password and prompt (redundant)
result = click_runner.invoke(anta, ["nrfu", "--enable", "--enable-password", "blah", "--prompt"], input="y\npassword\npassword\n")
assert "Is a password required to enter EOS privileged EXEC mode? [y/N]:" not in result.output
assert "Please enter a password to enter EOS privileged EXEC mode" not in result.output
@@ -91,17 +82,13 @@ def test_anta_enable_password(click_runner: CliRunner) -> None:
def test_anta_enable_alone(click_runner: CliRunner) -> None:
- """
- Test that enable can be provided either without enable-password
- """
+ """Test that enable can be provided either without enable-password."""
result = click_runner.invoke(anta, ["nrfu", "--enable"])
assert result.exit_code == ExitCode.OK
def test_disable_cache(click_runner: CliRunner) -> None:
- """
- Test that disable_cache is working on inventory
- """
+ """Test that disable_cache is working on inventory."""
result = click_runner.invoke(anta, ["nrfu", "--disable-cache"])
stdout_lines = result.stdout.split("\n")
# All caches should be disabled from the inventory
diff --git a/tests/units/cli/nrfu/test_commands.py b/tests/units/cli/nrfu/test_commands.py
index 4639671..4ea40b7 100644
--- a/tests/units/cli/nrfu/test_commands.py
+++ b/tests/units/cli/nrfu/test_commands.py
@@ -1,97 +1,82 @@
# 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.
-"""
-Tests for anta.cli.nrfu.commands
-"""
+"""Tests for anta.cli.nrfu.commands."""
+
from __future__ import annotations
import json
import re
from pathlib import Path
-
-from click.testing import CliRunner
+from typing import TYPE_CHECKING
from anta.cli import anta
from anta.cli.utils import ExitCode
+if TYPE_CHECKING:
+ from click.testing import CliRunner
+
DATA_DIR: Path = Path(__file__).parent.parent.parent.parent.resolve() / "data"
def test_anta_nrfu_table_help(click_runner: CliRunner) -> None:
- """
- Test anta nrfu table --help
- """
+ """Test anta nrfu table --help."""
result = click_runner.invoke(anta, ["nrfu", "table", "--help"])
assert result.exit_code == ExitCode.OK
assert "Usage: anta nrfu table" in result.output
def test_anta_nrfu_text_help(click_runner: CliRunner) -> None:
- """
- Test anta nrfu text --help
- """
+ """Test anta nrfu text --help."""
result = click_runner.invoke(anta, ["nrfu", "text", "--help"])
assert result.exit_code == ExitCode.OK
assert "Usage: anta nrfu text" in result.output
def test_anta_nrfu_json_help(click_runner: CliRunner) -> None:
- """
- Test anta nrfu json --help
- """
+ """Test anta nrfu json --help."""
result = click_runner.invoke(anta, ["nrfu", "json", "--help"])
assert result.exit_code == ExitCode.OK
assert "Usage: anta nrfu json" in result.output
def test_anta_nrfu_template_help(click_runner: CliRunner) -> None:
- """
- Test anta nrfu tpl-report --help
- """
+ """Test anta nrfu tpl-report --help."""
result = click_runner.invoke(anta, ["nrfu", "tpl-report", "--help"])
assert result.exit_code == ExitCode.OK
assert "Usage: anta nrfu tpl-report" in result.output
def test_anta_nrfu_table(click_runner: CliRunner) -> None:
- """
- Test anta nrfu, catalog is given via env
- """
+ """Test anta nrfu, catalog is given via env."""
result = click_runner.invoke(anta, ["nrfu", "table"])
assert result.exit_code == ExitCode.OK
assert "dummy │ VerifyEOSVersion │ success" in result.output
def test_anta_nrfu_text(click_runner: CliRunner) -> None:
- """
- Test anta nrfu, catalog is given via env
- """
+ """Test anta nrfu, catalog is given via env."""
result = click_runner.invoke(anta, ["nrfu", "text"])
assert result.exit_code == ExitCode.OK
assert "dummy :: VerifyEOSVersion :: SUCCESS" in result.output
def test_anta_nrfu_json(click_runner: CliRunner) -> None:
- """
- Test anta nrfu, catalog is given via env
- """
+ """Test anta nrfu, catalog is given via env."""
result = click_runner.invoke(anta, ["nrfu", "json"])
assert result.exit_code == ExitCode.OK
- assert "JSON results of all tests" in result.output
- m = re.search(r"\[\n {[\s\S]+ }\n\]", result.output)
- assert m is not None
- result_list = json.loads(m.group())
- for r in result_list:
- if r["name"] == "dummy":
- assert r["test"] == "VerifyEOSVersion"
- assert r["result"] == "success"
+ assert "JSON results" in result.output
+ match = re.search(r"\[\n {[\s\S]+ }\n\]", result.output)
+ assert match is not None
+ result_list = json.loads(match.group())
+ for res in result_list:
+ if res["name"] == "dummy":
+ assert res["test"] == "VerifyEOSVersion"
+ assert res["result"] == "success"
def test_anta_nrfu_template(click_runner: CliRunner) -> None:
- """
- Test anta nrfu, catalog is given via env
- """
+ """Test anta nrfu, catalog is given via env."""
result = click_runner.invoke(anta, ["nrfu", "tpl-report", "--template", str(DATA_DIR / "template.j2")])
assert result.exit_code == ExitCode.OK
assert "* VerifyEOSVersion is SUCCESS for dummy" in result.output
diff --git a/tests/units/cli/test__init__.py b/tests/units/cli/test__init__.py
index 0e84e14..0701083 100644
--- a/tests/units/cli/test__init__.py
+++ b/tests/units/cli/test__init__.py
@@ -1,58 +1,64 @@
# 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.
-"""
-Tests for anta.cli.__init__
-"""
+"""Tests for anta.cli.__init__."""
from __future__ import annotations
-from click.testing import CliRunner
+from typing import TYPE_CHECKING
+from unittest.mock import patch
-from anta.cli import anta
+import pytest
+
+from anta.cli import anta, cli
from anta.cli.utils import ExitCode
+if TYPE_CHECKING:
+ from click.testing import CliRunner
+
def test_anta(click_runner: CliRunner) -> None:
- """
- Test anta main entrypoint
- """
+ """Test anta main entrypoint."""
result = click_runner.invoke(anta)
assert result.exit_code == ExitCode.OK
assert "Usage" in result.output
def test_anta_help(click_runner: CliRunner) -> None:
- """
- Test anta --help
- """
+ """Test anta --help."""
result = click_runner.invoke(anta, ["--help"])
assert result.exit_code == ExitCode.OK
assert "Usage" in result.output
def test_anta_exec_help(click_runner: CliRunner) -> None:
- """
- Test anta exec --help
- """
+ """Test anta exec --help."""
result = click_runner.invoke(anta, ["exec", "--help"])
assert result.exit_code == ExitCode.OK
assert "Usage: anta exec" in result.output
def test_anta_debug_help(click_runner: CliRunner) -> None:
- """
- Test anta debug --help
- """
+ """Test anta debug --help."""
result = click_runner.invoke(anta, ["debug", "--help"])
assert result.exit_code == ExitCode.OK
assert "Usage: anta debug" in result.output
def test_anta_get_help(click_runner: CliRunner) -> None:
- """
- Test anta get --help
- """
+ """Test anta get --help."""
result = click_runner.invoke(anta, ["get", "--help"])
assert result.exit_code == ExitCode.OK
assert "Usage: anta get" in result.output
+
+
+def test_uncaught_failure_anta(caplog: pytest.LogCaptureFixture) -> None:
+ """Test uncaught failure when running ANTA cli."""
+ with (
+ pytest.raises(SystemExit) as e_info,
+ patch("anta.cli.anta", side_effect=ZeroDivisionError()),
+ ):
+ cli()
+ assert "CRITICAL" in caplog.text
+ assert "Uncaught Exception when running ANTA CLI" in caplog.text
+ assert e_info.value.code == 1
diff --git a/tests/units/inventory/__init__.py b/tests/units/inventory/__init__.py
index e772bee..70fbdda 100644
--- a/tests/units/inventory/__init__.py
+++ b/tests/units/inventory/__init__.py
@@ -1,3 +1,4 @@
# 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.
+"""Tests for inventory submodule."""
diff --git a/tests/units/inventory/test_inventory.py b/tests/units/inventory/test_inventory.py
index 7c62b5c..430ca21 100644
--- a/tests/units/inventory/test_inventory.py
+++ b/tests/units/inventory/test_inventory.py
@@ -2,23 +2,25 @@
# Use of this source code is governed by the Apache License 2.0
# that can be found in the LICENSE file.
"""ANTA Inventory unit tests."""
+
from __future__ import annotations
-import logging
-from pathlib import Path
-from typing import Any
+from typing import TYPE_CHECKING, Any
import pytest
import yaml
from pydantic import ValidationError
from anta.inventory import AntaInventory
-from anta.inventory.exceptions import InventoryIncorrectSchema, InventoryRootKeyError
+from anta.inventory.exceptions import InventoryIncorrectSchemaError, InventoryRootKeyError
from tests.data.json_data import ANTA_INVENTORY_TESTS_INVALID, ANTA_INVENTORY_TESTS_VALID
from tests.lib.utils import generate_test_ids_dict
+if TYPE_CHECKING:
+ from pathlib import Path
+
-class Test_AntaInventory:
+class TestAntaInventory:
"""Test AntaInventory class."""
def create_inventory(self, content: str, tmp_path: Path) -> str:
@@ -31,7 +33,7 @@ class Test_AntaInventory:
def check_parameter(self, parameter: str, test_definition: dict[Any, Any]) -> bool:
"""Check if parameter is configured in testbed."""
- return "parameters" in test_definition and parameter in test_definition["parameters"].keys()
+ return "parameters" in test_definition and parameter in test_definition["parameters"]
@pytest.mark.parametrize("test_definition", ANTA_INVENTORY_TESTS_VALID, ids=generate_test_ids_dict)
def test_init_valid(self, test_definition: dict[str, Any], tmp_path: Path) -> None:
@@ -55,8 +57,7 @@ class Test_AntaInventory:
try:
AntaInventory.parse(filename=inventory_file, username="arista", password="arista123")
except ValidationError as exc:
- logging.error("Exceptions is: %s", str(exc))
- assert False
+ raise AssertionError from exc
@pytest.mark.parametrize("test_definition", ANTA_INVENTORY_TESTS_INVALID, ids=generate_test_ids_dict)
def test_init_invalid(self, test_definition: dict[str, Any], tmp_path: Path) -> None:
@@ -77,5 +78,5 @@ class Test_AntaInventory:
"""
inventory_file = self.create_inventory(content=test_definition["input"], tmp_path=tmp_path)
- with pytest.raises((InventoryIncorrectSchema, InventoryRootKeyError, ValidationError)):
+ with pytest.raises((InventoryIncorrectSchemaError, InventoryRootKeyError, ValidationError)):
AntaInventory.parse(filename=inventory_file, username="arista", password="arista123")
diff --git a/tests/units/inventory/test_models.py b/tests/units/inventory/test_models.py
index 83f151c..0dccfb8 100644
--- a/tests/units/inventory/test_models.py
+++ b/tests/units/inventory/test_models.py
@@ -2,6 +2,7 @@
# Use of this source code is governed by the Apache License 2.0
# that can be found in the LICENSE file.
"""ANTA Inventory models unit tests."""
+
from __future__ import annotations
import logging
@@ -30,7 +31,7 @@ from tests.data.json_data import (
from tests.lib.utils import generate_test_ids_dict
-class Test_InventoryUnitModels:
+class TestInventoryUnitModels:
"""Test components of AntaInventoryInput model."""
@pytest.mark.parametrize("test_definition", INVENTORY_MODEL_HOST_VALID, ids=generate_test_ids_dict)
@@ -51,9 +52,8 @@ class Test_InventoryUnitModels:
host_inventory = AntaInventoryHost(host=test_definition["input"])
except ValidationError as exc:
logging.warning("Error: %s", str(exc))
- assert False
- else:
- assert test_definition["input"] == str(host_inventory.host)
+ raise AssertionError from exc
+ assert test_definition["input"] == str(host_inventory.host)
@pytest.mark.parametrize("test_definition", INVENTORY_MODEL_HOST_INVALID, ids=generate_test_ids_dict)
def test_anta_inventory_host_invalid(self, test_definition: dict[str, Any]) -> None:
@@ -110,9 +110,8 @@ class Test_InventoryUnitModels:
network_inventory = AntaInventoryNetwork(network=test_definition["input"])
except ValidationError as exc:
logging.warning("Error: %s", str(exc))
- assert False
- else:
- assert test_definition["input"] == str(network_inventory.network)
+ raise AssertionError from exc
+ assert test_definition["input"] == str(network_inventory.network)
@pytest.mark.parametrize("test_definition", INVENTORY_MODEL_NETWORK_INVALID, ids=generate_test_ids_dict)
def test_anta_inventory_network_invalid(self, test_definition: dict[str, Any]) -> None:
@@ -133,11 +132,11 @@ class Test_InventoryUnitModels:
except ValidationError as exc:
logging.warning("Error: %s", str(exc))
else:
- assert False
+ raise AssertionError
@pytest.mark.parametrize("test_definition", INVENTORY_MODEL_NETWORK_CACHE, ids=generate_test_ids_dict)
def test_anta_inventory_network_cache(self, test_definition: dict[str, Any]) -> None:
- """Test network disable_cache
+ """Test network disable_cache.
Test structure:
---------------
@@ -176,10 +175,9 @@ class Test_InventoryUnitModels:
)
except ValidationError as exc:
logging.warning("Error: %s", str(exc))
- assert False
- else:
- assert test_definition["input"]["start"] == str(range_inventory.start)
- assert test_definition["input"]["end"] == str(range_inventory.end)
+ raise AssertionError from exc
+ assert test_definition["input"]["start"] == str(range_inventory.start)
+ assert test_definition["input"]["end"] == str(range_inventory.end)
@pytest.mark.parametrize("test_definition", INVENTORY_MODEL_RANGE_INVALID, ids=generate_test_ids_dict)
def test_anta_inventory_range_invalid(self, test_definition: dict[str, Any]) -> None:
@@ -203,11 +201,11 @@ class Test_InventoryUnitModels:
except ValidationError as exc:
logging.warning("Error: %s", str(exc))
else:
- assert False
+ raise AssertionError
@pytest.mark.parametrize("test_definition", INVENTORY_MODEL_RANGE_CACHE, ids=generate_test_ids_dict)
def test_anta_inventory_range_cache(self, test_definition: dict[str, Any]) -> None:
- """Test range disable_cache
+ """Test range disable_cache.
Test structure:
---------------
@@ -221,22 +219,23 @@ class Test_InventoryUnitModels:
"""
if "disable_cache" in test_definition["input"]:
range_inventory = AntaInventoryRange(
- start=test_definition["input"]["start"], end=test_definition["input"]["end"], disable_cache=test_definition["input"]["disable_cache"]
+ start=test_definition["input"]["start"],
+ end=test_definition["input"]["end"],
+ disable_cache=test_definition["input"]["disable_cache"],
)
else:
range_inventory = AntaInventoryRange(start=test_definition["input"]["start"], end=test_definition["input"]["end"])
assert test_definition["expected_result"] == range_inventory.disable_cache
-class Test_AntaInventoryInputModel:
+class TestAntaInventoryInputModel:
"""Unit test of AntaInventoryInput model."""
def test_inventory_input_structure(self) -> None:
"""Test inventory keys are those expected."""
-
inventory = AntaInventoryInput()
logging.info("Inventory keys are: %s", str(inventory.model_dump().keys()))
- assert all(elem in inventory.model_dump().keys() for elem in ["hosts", "networks", "ranges"])
+ assert all(elem in inventory.model_dump() for elem in ["hosts", "networks", "ranges"])
@pytest.mark.parametrize("inventory_def", INVENTORY_MODEL_VALID, ids=generate_test_ids_dict)
def test_anta_inventory_intput_valid(self, inventory_def: dict[str, Any]) -> None:
@@ -265,10 +264,9 @@ class Test_AntaInventoryInputModel:
inventory = AntaInventoryInput(**inventory_def["input"])
except ValidationError as exc:
logging.warning("Error: %s", str(exc))
- assert False
- else:
- logging.info("Checking if all root keys are correctly lodaded")
- assert all(elem in inventory.model_dump().keys() for elem in inventory_def["input"].keys())
+ raise AssertionError from exc
+ logging.info("Checking if all root keys are correctly lodaded")
+ assert all(elem in inventory.model_dump() for elem in inventory_def["input"])
@pytest.mark.parametrize("inventory_def", INVENTORY_MODEL_INVALID, ids=generate_test_ids_dict)
def test_anta_inventory_intput_invalid(self, inventory_def: dict[str, Any]) -> None:
@@ -294,19 +292,19 @@ class Test_AntaInventoryInputModel:
"""
try:
- if "hosts" in inventory_def["input"].keys():
+ if "hosts" in inventory_def["input"]:
logging.info(
"Loading %s into AntaInventoryInput hosts section",
str(inventory_def["input"]["hosts"]),
)
AntaInventoryInput(hosts=inventory_def["input"]["hosts"])
- if "networks" in inventory_def["input"].keys():
+ if "networks" in inventory_def["input"]:
logging.info(
"Loading %s into AntaInventoryInput networks section",
str(inventory_def["input"]["networks"]),
)
AntaInventoryInput(networks=inventory_def["input"]["networks"])
- if "ranges" in inventory_def["input"].keys():
+ if "ranges" in inventory_def["input"]:
logging.info(
"Loading %s into AntaInventoryInput ranges section",
str(inventory_def["input"]["ranges"]),
@@ -315,10 +313,10 @@ class Test_AntaInventoryInputModel:
except ValidationError as exc:
logging.warning("Error: %s", str(exc))
else:
- assert False
+ raise AssertionError
-class Test_InventoryDeviceModel:
+class TestInventoryDeviceModel:
"""Unit test of InventoryDevice model."""
@pytest.mark.parametrize("test_definition", INVENTORY_DEVICE_MODEL_VALID, ids=generate_test_ids_dict)
@@ -349,12 +347,12 @@ class Test_InventoryDeviceModel:
if test_definition["expected_result"] == "invalid":
pytest.skip("Not concerned by the test")
- for entity in test_definition["input"]:
- try:
+ try:
+ for entity in test_definition["input"]:
AsyncEOSDevice(**entity)
- except TypeError as exc:
- logging.warning("Error: %s", str(exc))
- assert False
+ except TypeError as exc:
+ logging.warning("Error: %s", str(exc))
+ raise AssertionError from exc
@pytest.mark.parametrize("test_definition", INVENTORY_DEVICE_MODEL_INVALID, ids=generate_test_ids_dict)
def test_inventory_device_invalid(self, test_definition: dict[str, Any]) -> None:
@@ -384,10 +382,10 @@ class Test_InventoryDeviceModel:
if test_definition["expected_result"] == "valid":
pytest.skip("Not concerned by the test")
- for entity in test_definition["input"]:
- try:
+ try:
+ for entity in test_definition["input"]:
AsyncEOSDevice(**entity)
- except TypeError as exc:
- logging.info("Error: %s", str(exc))
- else:
- assert False
+ except TypeError as exc:
+ logging.info("Error: %s", str(exc))
+ else:
+ raise AssertionError
diff --git a/tests/units/reporter/__init__.py b/tests/units/reporter/__init__.py
index e772bee..6e606e5 100644
--- a/tests/units/reporter/__init__.py
+++ b/tests/units/reporter/__init__.py
@@ -1,3 +1,4 @@
# 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.
+"""Tests for anta.reporter submodule."""
diff --git a/tests/units/reporter/test__init__.py b/tests/units/reporter/test__init__.py
index 259942f..0dc9f9a 100644
--- a/tests/units/reporter/test__init__.py
+++ b/tests/units/reporter/test__init__.py
@@ -1,44 +1,51 @@
# 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.report.__init__.py
-"""
+"""Test anta.report.__init__.py."""
+
from __future__ import annotations
-from typing import Callable
+from typing import TYPE_CHECKING, Callable
import pytest
from rich.table import Table
from anta import RICH_COLOR_PALETTE
-from anta.custom_types import TestStatus
from anta.reporter import ReportTable
-from anta.result_manager import ResultManager
+
+if TYPE_CHECKING:
+ from anta.custom_types import TestStatus
+ from anta.result_manager import ResultManager
-class Test_ReportTable:
- """
- Test ReportTable class
- """
+class TestReportTable:
+ """Test ReportTable class."""
# not testing __init__ as nothing is going on there
@pytest.mark.parametrize(
- "usr_list, delimiter, expected_output",
+ ("usr_list", "delimiter", "expected_output"),
[
pytest.param([], None, "", id="empty list no delimiter"),
pytest.param([], "*", "", id="empty list with delimiter"),
pytest.param(["elem1"], None, "elem1", id="one elem list no delimiter"),
pytest.param(["elem1"], "*", "* elem1", id="one elem list with delimiter"),
- pytest.param(["elem1", "elem2"], None, "elem1\nelem2", id="two elems list no delimiter"),
- pytest.param(["elem1", "elem2"], "&", "& elem1\n& elem2", id="two elems list with delimiter"),
+ pytest.param(
+ ["elem1", "elem2"],
+ None,
+ "elem1\nelem2",
+ id="two elems list no delimiter",
+ ),
+ pytest.param(
+ ["elem1", "elem2"],
+ "&",
+ "& elem1\n& elem2",
+ id="two elems list with delimiter",
+ ),
],
)
def test__split_list_to_txt_list(self, usr_list: list[str], delimiter: str | None, expected_output: str) -> None:
- """
- test _split_list_to_txt_list
- """
+ """Test _split_list_to_txt_list."""
# pylint: disable=protected-access
report = ReportTable()
assert report._split_list_to_txt_list(usr_list, delimiter) == expected_output
@@ -52,9 +59,7 @@ class Test_ReportTable:
],
)
def test__build_headers(self, headers: list[str]) -> None:
- """
- test _build_headers
- """
+ """Test _build_headers."""
# pylint: disable=protected-access
report = ReportTable()
table = Table()
@@ -65,7 +70,7 @@ class Test_ReportTable:
assert table.columns[table_column_before].style == RICH_COLOR_PALETTE.HEADER
@pytest.mark.parametrize(
- "status, expected_status",
+ ("status", "expected_status"),
[
pytest.param("unknown", "unknown", id="unknown status"),
pytest.param("unset", "[grey74]unset", id="unset status"),
@@ -76,48 +81,42 @@ class Test_ReportTable:
],
)
def test__color_result(self, status: TestStatus, expected_status: str) -> None:
- """
- test _build_headers
- """
+ """Test _build_headers."""
# pylint: disable=protected-access
report = ReportTable()
assert report._color_result(status) == expected_status
@pytest.mark.parametrize(
- "host, testcase, title, number_of_tests, expected_length",
+ ("title", "number_of_tests", "expected_length"),
[
- pytest.param(None, None, None, 5, 5, id="all results"),
- pytest.param("host1", None, None, 5, 0, id="result for host1 when no host1 test"),
- pytest.param(None, "VerifyTest3", None, 5, 1, id="result for test VerifyTest3"),
- pytest.param(None, None, "Custom title", 5, 5, id="Change table title"),
+ pytest.param(None, 5, 5, id="all results"),
+ pytest.param(None, 0, 0, id="result for host1 when no host1 test"),
+ pytest.param(None, 5, 5, id="result for test VerifyTest3"),
+ pytest.param("Custom title", 5, 5, id="Change table title"),
],
)
def test_report_all(
self,
result_manager_factory: Callable[[int], ResultManager],
- host: str | None,
- testcase: str | None,
title: str | None,
number_of_tests: int,
expected_length: int,
) -> None:
- """
- test report_all
- """
+ """Test report_all."""
# pylint: disable=too-many-arguments
- rm = result_manager_factory(number_of_tests)
+ manager = result_manager_factory(number_of_tests)
report = ReportTable()
- kwargs = {"host": host, "testcase": testcase, "title": title}
+ kwargs = {"title": title}
kwargs = {k: v for k, v in kwargs.items() if v is not None}
- res = report.report_all(rm, **kwargs) # type: ignore[arg-type]
+ res = report.report_all(manager, **kwargs) # type: ignore[arg-type]
assert isinstance(res, Table)
assert res.title == (title or "All tests results")
assert res.row_count == expected_length
@pytest.mark.parametrize(
- "testcase, title, number_of_tests, expected_length",
+ ("test", "title", "number_of_tests", "expected_length"),
[
pytest.param(None, None, 5, 5, id="all results"),
pytest.param("VerifyTest3", None, 5, 1, id="result for test VerifyTest3"),
@@ -127,67 +126,62 @@ class Test_ReportTable:
def test_report_summary_tests(
self,
result_manager_factory: Callable[[int], ResultManager],
- testcase: str | None,
+ test: str | None,
title: str | None,
number_of_tests: int,
expected_length: int,
) -> None:
- """
- test report_summary_tests
- """
+ """Test report_summary_tests."""
# pylint: disable=too-many-arguments
- # TODO refactor this later... this is injecting double test results by modyfing the device name
+ # TODO: refactor this later... this is injecting double test results by modyfing the device name
# should be a fixture
- rm = result_manager_factory(number_of_tests)
- new_results = [result.model_copy() for result in rm.get_results()]
+ manager = result_manager_factory(number_of_tests)
+ new_results = [result.model_copy() for result in manager.results]
for result in new_results:
result.name = "test_device"
result.result = "failure"
- rm.add_test_results(new_results)
report = ReportTable()
- kwargs = {"testcase": testcase, "title": title}
+ kwargs = {"tests": [test] if test is not None else None, "title": title}
kwargs = {k: v for k, v in kwargs.items() if v is not None}
- res = report.report_summary_tests(rm, **kwargs) # type: ignore[arg-type]
+ res = report.report_summary_tests(manager, **kwargs) # type: ignore[arg-type]
assert isinstance(res, Table)
- assert res.title == (title or "Summary per test case")
+ assert res.title == (title or "Summary per test")
assert res.row_count == expected_length
@pytest.mark.parametrize(
- "host, title, number_of_tests, expected_length",
+ ("dev", "title", "number_of_tests", "expected_length"),
[
- pytest.param(None, None, 5, 2, id="all results"),
- pytest.param("host1", None, 5, 1, id="result for host host1"),
- pytest.param(None, "Custom title", 5, 2, id="Change table title"),
+ pytest.param(None, None, 5, 1, id="all results"),
+ pytest.param("device1", None, 5, 1, id="result for host host1"),
+ pytest.param(None, "Custom title", 5, 1, id="Change table title"),
],
)
- def test_report_summary_hosts(
+ def test_report_summary_devices(
self,
result_manager_factory: Callable[[int], ResultManager],
- host: str | None,
+ dev: str | None,
title: str | None,
number_of_tests: int,
expected_length: int,
) -> None:
- """
- test report_summary_hosts
- """
+ """Test report_summary_devices."""
# pylint: disable=too-many-arguments
- # TODO refactor this later... this is injecting double test results by modyfing the device name
+ # TODO: refactor this later... this is injecting double test results by modyfing the device name
# should be a fixture
- rm = result_manager_factory(number_of_tests)
- new_results = [result.model_copy() for result in rm.get_results()]
+ manager = result_manager_factory(number_of_tests)
+ new_results = [result.model_copy() for result in manager.results]
for result in new_results:
- result.name = host or "test_device"
+ result.name = dev or "test_device"
result.result = "failure"
- rm.add_test_results(new_results)
+ manager.results = new_results
report = ReportTable()
- kwargs = {"host": host, "title": title}
+ kwargs = {"devices": [dev] if dev is not None else None, "title": title}
kwargs = {k: v for k, v in kwargs.items() if v is not None}
- res = report.report_summary_hosts(rm, **kwargs) # type: ignore[arg-type]
+ res = report.report_summary_devices(manager, **kwargs) # type: ignore[arg-type]
assert isinstance(res, Table)
- assert res.title == (title or "Summary per host")
+ assert res.title == (title or "Summary per device")
assert res.row_count == expected_length
diff --git a/tests/units/result_manager/__init__.py b/tests/units/result_manager/__init__.py
index e772bee..861145b 100644
--- a/tests/units/result_manager/__init__.py
+++ b/tests/units/result_manager/__init__.py
@@ -1,3 +1,4 @@
# 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.
+"""Tests for anta.result_manager submodule."""
diff --git a/tests/units/result_manager/test__init__.py b/tests/units/result_manager/test__init__.py
index c457c84..02c694c 100644
--- a/tests/units/result_manager/test__init__.py
+++ b/tests/units/result_manager/test__init__.py
@@ -1,204 +1,277 @@
# 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.result_manager.__init__.py
-"""
+"""Test anta.result_manager.__init__.py."""
+
from __future__ import annotations
import json
-from contextlib import nullcontext
-from typing import TYPE_CHECKING, Any, Callable
+from contextlib import AbstractContextManager, nullcontext
+from typing import TYPE_CHECKING, Callable
import pytest
-from anta.custom_types import TestStatus
-from anta.result_manager import ResultManager
+from anta.result_manager import ResultManager, models
if TYPE_CHECKING:
+ from anta.custom_types import TestStatus
from anta.result_manager.models import TestResult
-class Test_ResultManager:
- """
- Test ResultManager class
- """
+class TestResultManager:
+ """Test ResultManager class."""
# not testing __init__ as nothing is going on there
def test__len__(self, list_result_factory: Callable[[int], list[TestResult]]) -> None:
- """
- test __len__
- """
+ """Test __len__."""
list_result = list_result_factory(3)
result_manager = ResultManager()
assert len(result_manager) == 0
for i in range(3):
- result_manager.add_test_result(list_result[i])
+ result_manager.add(list_result[i])
assert len(result_manager) == i + 1
+ def test_results_getter(self, result_manager_factory: Callable[[int], ResultManager]) -> None:
+ """Test ResultManager.results property getter."""
+ result_manager = result_manager_factory(3)
+ res = result_manager.results
+ assert len(res) == 3
+ assert isinstance(res, list)
+ for e in res:
+ assert isinstance(e, models.TestResult)
+
+ def test_results_setter(self, list_result_factory: Callable[[int], list[TestResult]], result_manager_factory: Callable[[int], ResultManager]) -> None:
+ """Test ResultManager.results property setter."""
+ result_manager = result_manager_factory(3)
+ assert len(result_manager) == 3
+ tests = list_result_factory(5)
+ result_manager.results = tests
+ assert len(result_manager) == 5
+
+ def test_json(self, list_result_factory: Callable[[int], list[TestResult]]) -> None:
+ """Test ResultManager.json property."""
+ result_manager = ResultManager()
+
+ success_list = list_result_factory(3)
+ for test in success_list:
+ test.result = "success"
+ result_manager.results = success_list
+
+ json_res = result_manager.json
+ assert isinstance(json_res, str)
+
+ # Verifies it can be deserialized back to a list of dict with the correct values types
+ res = json.loads(json_res)
+ for test in res:
+ assert isinstance(test, dict)
+ assert isinstance(test.get("test"), str)
+ assert isinstance(test.get("categories"), list)
+ assert isinstance(test.get("description"), str)
+ assert test.get("custom_field") is None
+ assert test.get("result") == "success"
+
@pytest.mark.parametrize(
- "starting_status, test_status, expected_status, expected_raise",
+ ("starting_status", "test_status", "expected_status", "expected_raise"),
[
pytest.param("unset", "unset", "unset", nullcontext(), id="unset->unset"),
pytest.param("unset", "success", "success", nullcontext(), id="unset->success"),
pytest.param("unset", "error", "unset", nullcontext(), id="set error"),
pytest.param("skipped", "skipped", "skipped", nullcontext(), id="skipped->skipped"),
pytest.param("skipped", "unset", "skipped", nullcontext(), id="skipped, add unset"),
- pytest.param("skipped", "success", "success", nullcontext(), id="skipped, add success"),
- pytest.param("skipped", "failure", "failure", nullcontext(), id="skipped, add failure"),
+ pytest.param(
+ "skipped",
+ "success",
+ "success",
+ nullcontext(),
+ id="skipped, add success",
+ ),
+ pytest.param(
+ "skipped",
+ "failure",
+ "failure",
+ nullcontext(),
+ id="skipped, add failure",
+ ),
pytest.param("success", "unset", "success", nullcontext(), id="success, add unset"),
- pytest.param("success", "skipped", "success", nullcontext(), id="success, add skipped"),
+ pytest.param(
+ "success",
+ "skipped",
+ "success",
+ nullcontext(),
+ id="success, add skipped",
+ ),
pytest.param("success", "success", "success", nullcontext(), id="success->success"),
pytest.param("success", "failure", "failure", nullcontext(), id="success->failure"),
pytest.param("failure", "unset", "failure", nullcontext(), id="failure->failure"),
pytest.param("failure", "skipped", "failure", nullcontext(), id="failure, add unset"),
- pytest.param("failure", "success", "failure", nullcontext(), id="failure, add skipped"),
- pytest.param("failure", "failure", "failure", nullcontext(), id="failure, add success"),
- pytest.param("unset", "unknown", None, pytest.raises(ValueError), id="wrong status"),
+ pytest.param(
+ "failure",
+ "success",
+ "failure",
+ nullcontext(),
+ id="failure, add skipped",
+ ),
+ pytest.param(
+ "failure",
+ "failure",
+ "failure",
+ nullcontext(),
+ id="failure, add success",
+ ),
+ pytest.param(
+ "unset", "unknown", None, pytest.raises(ValueError, match="Input should be 'unset', 'success', 'failure', 'error' or 'skipped'"), id="wrong status"
+ ),
],
)
- def test__update_status(self, starting_status: TestStatus, test_status: TestStatus, expected_status: str, expected_raise: Any) -> None:
- """
- Test ResultManager._update_status
- """
+ def test_add(
+ self,
+ test_result_factory: Callable[[], TestResult],
+ starting_status: TestStatus,
+ test_status: TestStatus,
+ expected_status: str,
+ expected_raise: AbstractContextManager[Exception],
+ ) -> None:
+ # pylint: disable=too-many-arguments
+ """Test ResultManager_update_status."""
result_manager = ResultManager()
result_manager.status = starting_status
assert result_manager.error_status is False
+ assert len(result_manager) == 0
+ test = test_result_factory()
+ test.result = test_status
with expected_raise:
- result_manager._update_status(test_status) # pylint: disable=protected-access
+ result_manager.add(test)
if test_status == "error":
assert result_manager.error_status is True
else:
assert result_manager.status == expected_status
-
- def test_add_test_result(self, test_result_factory: Callable[[int], TestResult]) -> None:
- """
- Test ResultManager.add_test_result
- """
- result_manager = ResultManager()
- assert result_manager.status == "unset"
- assert result_manager.error_status is False
- assert len(result_manager) == 0
-
- # Add one unset test
- unset_test = test_result_factory(0)
- unset_test.result = "unset"
- result_manager.add_test_result(unset_test)
- assert result_manager.status == "unset"
- assert result_manager.error_status is False
- assert len(result_manager) == 1
-
- # Add one success test
- success_test = test_result_factory(1)
- success_test.result = "success"
- result_manager.add_test_result(success_test)
- assert result_manager.status == "success"
- assert result_manager.error_status is False
- assert len(result_manager) == 2
-
- # Add one error test
- error_test = test_result_factory(1)
- error_test.result = "error"
- result_manager.add_test_result(error_test)
- assert result_manager.status == "success"
- assert result_manager.error_status is True
- assert len(result_manager) == 3
-
- # Add one failure test
- failure_test = test_result_factory(1)
- failure_test.result = "failure"
- result_manager.add_test_result(failure_test)
- assert result_manager.status == "failure"
- assert result_manager.error_status is True
- assert len(result_manager) == 4
-
- def test_add_test_results(self, list_result_factory: Callable[[int], list[TestResult]]) -> None:
- """
- Test ResultManager.add_test_results
- """
- result_manager = ResultManager()
- assert result_manager.status == "unset"
- assert result_manager.error_status is False
- assert len(result_manager) == 0
-
- # Add three success tests
- success_list = list_result_factory(3)
- for test in success_list:
- test.result = "success"
- result_manager.add_test_results(success_list)
- assert result_manager.status == "success"
- assert result_manager.error_status is False
- assert len(result_manager) == 3
-
- # Add one error test and one failure
- error_failure_list = list_result_factory(2)
- error_failure_list[0].result = "error"
- error_failure_list[1].result = "failure"
- result_manager.add_test_results(error_failure_list)
- assert result_manager.status == "failure"
- assert result_manager.error_status is True
- assert len(result_manager) == 5
+ assert len(result_manager) == 1
@pytest.mark.parametrize(
- "status, error_status, ignore_error, expected_status",
+ ("status", "error_status", "ignore_error", "expected_status"),
[
pytest.param("success", False, True, "success", id="no error"),
pytest.param("success", True, True, "success", id="error, ignore error"),
pytest.param("success", True, False, "error", id="error, do not ignore error"),
],
)
- def test_get_status(self, status: TestStatus, error_status: bool, ignore_error: bool, expected_status: str) -> None:
- """
- test ResultManager.get_status
- """
+ def test_get_status(
+ self,
+ status: TestStatus,
+ error_status: bool,
+ ignore_error: bool,
+ expected_status: str,
+ ) -> None:
+ """Test ResultManager.get_status."""
result_manager = ResultManager()
result_manager.status = status
result_manager.error_status = error_status
assert result_manager.get_status(ignore_error=ignore_error) == expected_status
- def test_get_results(self, list_result_factory: Callable[[int], list[TestResult]]) -> None:
- """
- test ResultManager.get_results
- """
+ def test_filter(self, test_result_factory: Callable[[], TestResult], list_result_factory: Callable[[int], list[TestResult]]) -> None:
+ """Test ResultManager.filter."""
result_manager = ResultManager()
success_list = list_result_factory(3)
for test in success_list:
test.result = "success"
- result_manager.add_test_results(success_list)
+ result_manager.results = success_list
- res = result_manager.get_results()
- assert isinstance(res, list)
+ test = test_result_factory()
+ test.result = "failure"
+ result_manager.add(test)
+
+ test = test_result_factory()
+ test.result = "error"
+ result_manager.add(test)
+
+ test = test_result_factory()
+ test.result = "skipped"
+ result_manager.add(test)
+
+ assert len(result_manager) == 6
+ assert len(result_manager.filter({"failure"})) == 5
+ assert len(result_manager.filter({"error"})) == 5
+ assert len(result_manager.filter({"skipped"})) == 5
+ assert len(result_manager.filter({"failure", "error"})) == 4
+ assert len(result_manager.filter({"failure", "error", "skipped"})) == 3
+ assert len(result_manager.filter({"success", "failure", "error", "skipped"})) == 0
+
+ def test_get_by_tests(self, test_result_factory: Callable[[], TestResult], result_manager_factory: Callable[[int], ResultManager]) -> None:
+ """Test ResultManager.get_by_tests."""
+ result_manager = result_manager_factory(3)
+
+ test = test_result_factory()
+ test.test = "Test1"
+ result_manager.add(test)
+
+ test = test_result_factory()
+ test.test = "Test2"
+ result_manager.add(test)
+
+ test = test_result_factory()
+ test.test = "Test2"
+ result_manager.add(test)
+
+ assert len(result_manager) == 6
+ assert len(result_manager.filter_by_tests({"Test1"})) == 1
+ rm = result_manager.filter_by_tests({"Test1", "Test2"})
+ assert len(rm) == 3
+ assert len(rm.filter_by_tests({"Test1"})) == 1
+
+ def test_get_by_devices(self, test_result_factory: Callable[[], TestResult], result_manager_factory: Callable[[int], ResultManager]) -> None:
+ """Test ResultManager.get_by_devices."""
+ result_manager = result_manager_factory(3)
+
+ test = test_result_factory()
+ test.name = "Device1"
+ result_manager.add(test)
+
+ test = test_result_factory()
+ test.name = "Device2"
+ result_manager.add(test)
- def test_get_json_results(self, list_result_factory: Callable[[int], list[TestResult]]) -> None:
- """
- test ResultManager.get_json_results
- """
+ test = test_result_factory()
+ test.name = "Device2"
+ result_manager.add(test)
+
+ assert len(result_manager) == 6
+ assert len(result_manager.filter_by_devices({"Device1"})) == 1
+ rm = result_manager.filter_by_devices({"Device1", "Device2"})
+ assert len(rm) == 3
+ assert len(rm.filter_by_devices({"Device1"})) == 1
+
+ def test_get_tests(self, test_result_factory: Callable[[], TestResult], list_result_factory: Callable[[int], list[TestResult]]) -> None:
+ """Test ResultManager.get_tests."""
result_manager = ResultManager()
- success_list = list_result_factory(3)
- for test in success_list:
- test.result = "success"
- result_manager.add_test_results(success_list)
+ tests = list_result_factory(3)
+ for test in tests:
+ test.test = "Test1"
+ result_manager.results = tests
- json_res = result_manager.get_json_results()
- assert isinstance(json_res, str)
+ test = test_result_factory()
+ test.test = "Test2"
+ result_manager.add(test)
- # Verifies it can be deserialized back to a list of dict with the correct values types
- res = json.loads(json_res)
- for test in res:
- assert isinstance(test, dict)
- assert isinstance(test.get("test"), str)
- assert isinstance(test.get("categories"), list)
- assert isinstance(test.get("description"), str)
- assert test.get("custom_field") is None
- assert test.get("result") == "success"
+ assert len(result_manager.get_tests()) == 2
+ assert all(t in result_manager.get_tests() for t in ["Test1", "Test2"])
+
+ def test_get_devices(self, test_result_factory: Callable[[], TestResult], list_result_factory: Callable[[int], list[TestResult]]) -> None:
+ """Test ResultManager.get_tests."""
+ result_manager = ResultManager()
+
+ tests = list_result_factory(3)
+ for test in tests:
+ test.name = "Device1"
+ result_manager.results = tests
+
+ test = test_result_factory()
+ test.name = "Device2"
+ result_manager.add(test)
- # TODO
- # get_result_by_test
- # get_result_by_host
- # get_testcases
- # get_hosts
+ assert len(result_manager.get_devices()) == 2
+ assert all(t in result_manager.get_devices() for t in ["Device1", "Device2"])
diff --git a/tests/units/result_manager/test_models.py b/tests/units/result_manager/test_models.py
index bc7ba8a..2276153 100644
--- a/tests/units/result_manager/test_models.py
+++ b/tests/units/result_manager/test_models.py
@@ -2,18 +2,21 @@
# Use of this source code is governed by the Apache License 2.0
# that can be found in the LICENSE file.
"""ANTA Result Manager models unit tests."""
+
from __future__ import annotations
-from typing import Any, Callable
+from typing import TYPE_CHECKING, Any, Callable
import pytest
# Import as Result to avoid pytest collection
-from anta.result_manager.models import TestResult as Result
from tests.data.json_data import TEST_RESULT_SET_STATUS
from tests.lib.fixture import DEVICE_NAME
from tests.lib.utils import generate_test_ids_dict
+if TYPE_CHECKING:
+ from anta.result_manager.models import TestResult as Result
+
class TestTestResultModels:
"""Test components of anta.result_manager.models."""
diff --git a/tests/units/test_catalog.py b/tests/units/test_catalog.py
index 22a2121..8de6382 100644
--- a/tests/units/test_catalog.py
+++ b/tests/units/test_catalog.py
@@ -1,9 +1,8 @@
# 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
-"""
+"""test anta.device.py."""
+
from __future__ import annotations
from pathlib import Path
@@ -51,14 +50,21 @@ INIT_CATALOG_DATA: list[dict[str, Any]] = [
VerifyUptime,
VerifyUptime.Input(
minimum=10,
- filters=VerifyUptime.Input.Filters(tags=["fabric"]),
+ filters=VerifyUptime.Input.Filters(tags={"fabric"}),
+ ),
+ ),
+ (
+ VerifyUptime,
+ VerifyUptime.Input(
+ minimum=9,
+ filters=VerifyUptime.Input.Filters(tags={"leaf"}),
),
),
(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"]))),
+ (VerifyCPUUtilization, VerifyCPUUtilization.Input(filters=VerifyCPUUtilization.Input.Filters(tags={"leaf"}))),
+ (VerifyMemoryUtilization, VerifyMemoryUtilization.Input(filters=VerifyMemoryUtilization.Input.Filters(tags={"testdevice"}))),
(VerifyFileSystemUtilization, None),
(VerifyNTP, {}),
(VerifyMlagStatus, None),
@@ -146,12 +152,12 @@ CATALOG_FROM_LIST_FAIL_DATA: list[dict[str, Any]] = [
{
"name": "no_input_when_required",
"tests": [(FakeTestWithInput, None)],
- "error": "Field required",
+ "error": "FakeTestWithInput test inputs are not valid: 1 validation error for Input\n\tstring\n\t Field required",
},
{
"name": "wrong_input_type",
- "tests": [(FakeTestWithInput, True)],
- "error": "Value error, Coud not instantiate inputs as type bool is not valid",
+ "tests": [(FakeTestWithInput, {"string": True})],
+ "error": "FakeTestWithInput test inputs are not valid: 1 validation error for Input\n\tstring\n\t Input should be a valid string",
},
]
@@ -169,64 +175,52 @@ TESTS_SETTER_FAIL_DATA: list[dict[str, Any]] = [
]
-class Test_AntaCatalog:
- """
- Test for anta.catalog.AntaCatalog
- """
+class TestAntaCatalog:
+ """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
- """
+ """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"]):
+ for test_id, (test, inputs_data) 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)
+ if inputs_data is not None:
+ inputs = test.Input(**inputs_data) if isinstance(inputs_data, dict) else inputs_data
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
- """
+ """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"]):
+ for test_id, (test, inputs_data) 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)
+ if inputs_data is not None:
+ inputs = test.Input(**inputs_data) if isinstance(inputs_data, dict) else inputs_data
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:
+ """Instantiate AntaCatalog from a dict."""
+ file = DATA_DIR / catalog_data["filename"]
+ with file.open(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"]):
+ for test_id, (test, inputs_data) 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)
+ if inputs_data is not None:
+ inputs = test.Input(**inputs_data) if isinstance(inputs_data, dict) else inputs_data
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:
+ """Errors when instantiating AntaCatalog from a file."""
+ with pytest.raises((ValidationError, TypeError)) 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"]
@@ -234,34 +228,29 @@ class Test_AntaCatalog:
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:
+ """Errors when instantiating AntaCatalog from a file."""
+ with pytest.raises(FileNotFoundError) 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
+ 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
- """
+ """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:
+ """Errors when instantiating AntaCatalog from a list of tuples."""
+ file = DATA_DIR / catalog_data["filename"]
+ with file.open(encoding="UTF-8") as file:
data = safe_load(file)
- with pytest.raises((ValidationError, ValueError)) as exec_info:
+ with pytest.raises((ValidationError, TypeError)) as exec_info:
AntaCatalog.from_dict(data)
if isinstance(exec_info.value, ValidationError):
assert catalog_data["error"] in exec_info.value.errors()[0]["msg"]
@@ -269,9 +258,7 @@ class Test_AntaCatalog:
assert catalog_data["error"] in str(exec_info)
def test_filename(self) -> None:
- """
- Test filename
- """
+ """Test filename."""
catalog = AntaCatalog(filename="test")
assert catalog.filename == Path("test")
catalog = AntaCatalog(filename=Path("test"))
@@ -279,33 +266,34 @@ class Test_AntaCatalog:
@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
- """
+ """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"]):
+ for test_id, (test, inputs_data) 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)
+ if inputs_data is not None:
+ inputs = test.Input(**inputs_data) if isinstance(inputs_data, dict) else inputs_data
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
- """
+ """Errors when setting AntaCatalog.tests from a list of tuples."""
catalog = AntaCatalog()
- with pytest.raises(ValueError) as exec_info:
+ with pytest.raises(TypeError) 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()
- """
+ """Test AntaCatalog.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"])
+ tests: list[AntaTestDefinition] = catalog.get_tests_by_tags(tags={"leaf"})
+ assert len(tests) == 3
+ tests = catalog.get_tests_by_tags(tags={"leaf"}, strict=True)
assert len(tests) == 2
+
+ def test_get_tests_by_names(self) -> None:
+ """Test AntaCatalog.get_tests_by_tags()."""
+ catalog: AntaCatalog = AntaCatalog.parse(str(DATA_DIR / "test_catalog_with_tags.yml"))
+ tests: list[AntaTestDefinition] = catalog.get_tests_by_names(names={"VerifyUptime", "VerifyCoredump"})
+ assert len(tests) == 3
diff --git a/tests/units/test_device.py b/tests/units/test_device.py
index 845da2b..c901a3d 100644
--- a/tests/units/test_device.py
+++ b/tests/units/test_device.py
@@ -1,20 +1,17 @@
# 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
-"""
+"""test anta.device.py."""
from __future__ import annotations
import asyncio
from pathlib import Path
-from typing import Any
+from typing import TYPE_CHECKING, Any
from unittest.mock import patch
import httpx
import pytest
-from _pytest.mark.structures import ParameterSet
from asyncssh import SSHClientConnection, SSHClientConnectionOptions
from rich import print as rprint
@@ -24,6 +21,9 @@ from anta.models import AntaCommand
from tests.lib.fixture import COMMAND_OUTPUT
from tests.lib.utils import generate_test_ids_list
+if TYPE_CHECKING:
+ from _pytest.mark.structures import ParameterSet
+
INIT_DATA: list[dict[str, Any]] = [
{
"name": "no name, no port",
@@ -155,8 +155,8 @@ AIOEAPI_COLLECT_DATA: list[dict[str, Any]] = [
"memTotal": 8099732,
"memFree": 4989568,
"isIntlVersion": False,
- }
- ]
+ },
+ ],
},
},
"expected": {
@@ -211,7 +211,7 @@ AIOEAPI_COLLECT_DATA: list[dict[str, Any]] = [
"memFree": 4989568,
"isIntlVersion": False,
},
- ]
+ ],
},
},
"expected": {
@@ -266,7 +266,7 @@ AIOEAPI_COLLECT_DATA: list[dict[str, Any]] = [
"memFree": 4989568,
"isIntlVersion": False,
},
- ]
+ ],
},
},
"expected": {
@@ -322,7 +322,7 @@ AIOEAPI_COLLECT_DATA: list[dict[str, Any]] = [
"memFree": 4989568,
"isIntlVersion": False,
},
- ]
+ ],
},
},
"expected": {
@@ -356,8 +356,12 @@ AIOEAPI_COLLECT_DATA: list[dict[str, Any]] = [
"command": "show version",
"patch_kwargs": {
"side_effect": aioeapi.EapiCommandError(
- passed=[], failed="show version", errors=["Authorization denied for command 'show version'"], errmsg="Invalid command", not_exec=[]
- )
+ passed=[],
+ failed="show version",
+ errors=["Authorization denied for command 'show version'"],
+ errmsg="Invalid command",
+ not_exec=[],
+ ),
},
},
"expected": {"output": None, "errors": ["Authorization denied for command 'show version'"]},
@@ -369,7 +373,7 @@ AIOEAPI_COLLECT_DATA: list[dict[str, Any]] = [
"command": "show version",
"patch_kwargs": {"side_effect": httpx.HTTPError(message="404")},
},
- "expected": {"output": None, "errors": ["404"]},
+ "expected": {"output": None, "errors": ["HTTPError: 404"]},
},
{
"name": "httpx.ConnectError",
@@ -378,7 +382,7 @@ AIOEAPI_COLLECT_DATA: list[dict[str, Any]] = [
"command": "show version",
"patch_kwargs": {"side_effect": httpx.ConnectError(message="Cannot open port")},
},
- "expected": {"output": None, "errors": ["Cannot open port"]},
+ "expected": {"output": None, "errors": ["ConnectError: Cannot open port"]},
},
]
AIOEAPI_COPY_DATA: list[dict[str, Any]] = [
@@ -387,7 +391,7 @@ AIOEAPI_COPY_DATA: list[dict[str, Any]] = [
"device": {},
"copy": {
"sources": [Path("/mnt/flash"), Path("/var/log/agents")],
- "destination": Path("."),
+ "destination": Path(),
"direction": "from",
},
},
@@ -396,7 +400,7 @@ AIOEAPI_COPY_DATA: list[dict[str, Any]] = [
"device": {},
"copy": {
"sources": [Path("/mnt/flash"), Path("/var/log/agents")],
- "destination": Path("."),
+ "destination": Path(),
"direction": "to",
},
},
@@ -405,7 +409,7 @@ AIOEAPI_COPY_DATA: list[dict[str, Any]] = [
"device": {},
"copy": {
"sources": [Path("/mnt/flash"), Path("/var/log/agents")],
- "destination": Path("."),
+ "destination": Path(),
"direction": "wrong",
},
},
@@ -417,26 +421,28 @@ REFRESH_DATA: list[dict[str, Any]] = [
"patch_kwargs": (
{"return_value": True},
{
- "return_value": {
- "mfgName": "Arista",
- "modelName": "DCS-7280CR3-32P4-F",
- "hardwareRevision": "11.00",
- "serialNumber": "JPE19500066",
- "systemMacAddress": "fc:bd:67:3d:13:c5",
- "hwMacAddress": "fc:bd:67:3d:13:c5",
- "configMacAddress": "00:00:00:00:00:00",
- "version": "4.31.1F-34361447.fraserrel (engineering build)",
- "architecture": "x86_64",
- "internalVersion": "4.31.1F-34361447.fraserrel",
- "internalBuildId": "4940d112-a2fc-4970-8b5a-a16cd03fd08c",
- "imageFormatVersion": "3.0",
- "imageOptimization": "Default",
- "bootupTimestamp": 1700729434.5892005,
- "uptime": 20666.78,
- "memTotal": 8099732,
- "memFree": 4989568,
- "isIntlVersion": False,
- }
+ "return_value": [
+ {
+ "mfgName": "Arista",
+ "modelName": "DCS-7280CR3-32P4-F",
+ "hardwareRevision": "11.00",
+ "serialNumber": "JPE19500066",
+ "systemMacAddress": "fc:bd:67:3d:13:c5",
+ "hwMacAddress": "fc:bd:67:3d:13:c5",
+ "configMacAddress": "00:00:00:00:00:00",
+ "version": "4.31.1F-34361447.fraserrel (engineering build)",
+ "architecture": "x86_64",
+ "internalVersion": "4.31.1F-34361447.fraserrel",
+ "internalBuildId": "4940d112-a2fc-4970-8b5a-a16cd03fd08c",
+ "imageFormatVersion": "3.0",
+ "imageOptimization": "Default",
+ "bootupTimestamp": 1700729434.5892005,
+ "uptime": 20666.78,
+ "memTotal": 8099732,
+ "memFree": 4989568,
+ "isIntlVersion": False,
+ }
+ ],
},
),
"expected": {"is_online": True, "established": True, "hw_model": "DCS-7280CR3-32P4-F"},
@@ -466,7 +472,7 @@ REFRESH_DATA: list[dict[str, Any]] = [
"memTotal": 8099732,
"memFree": 4989568,
"isIntlVersion": False,
- }
+ },
},
),
"expected": {"is_online": False, "established": False, "hw_model": None},
@@ -477,25 +483,27 @@ REFRESH_DATA: list[dict[str, Any]] = [
"patch_kwargs": (
{"return_value": True},
{
- "return_value": {
- "mfgName": "Arista",
- "hardwareRevision": "11.00",
- "serialNumber": "JPE19500066",
- "systemMacAddress": "fc:bd:67:3d:13:c5",
- "hwMacAddress": "fc:bd:67:3d:13:c5",
- "configMacAddress": "00:00:00:00:00:00",
- "version": "4.31.1F-34361447.fraserrel (engineering build)",
- "architecture": "x86_64",
- "internalVersion": "4.31.1F-34361447.fraserrel",
- "internalBuildId": "4940d112-a2fc-4970-8b5a-a16cd03fd08c",
- "imageFormatVersion": "3.0",
- "imageOptimization": "Default",
- "bootupTimestamp": 1700729434.5892005,
- "uptime": 20666.78,
- "memTotal": 8099732,
- "memFree": 4989568,
- "isIntlVersion": False,
- }
+ "return_value": [
+ {
+ "mfgName": "Arista",
+ "hardwareRevision": "11.00",
+ "serialNumber": "JPE19500066",
+ "systemMacAddress": "fc:bd:67:3d:13:c5",
+ "hwMacAddress": "fc:bd:67:3d:13:c5",
+ "configMacAddress": "00:00:00:00:00:00",
+ "version": "4.31.1F-34361447.fraserrel (engineering build)",
+ "architecture": "x86_64",
+ "internalVersion": "4.31.1F-34361447.fraserrel",
+ "internalBuildId": "4940d112-a2fc-4970-8b5a-a16cd03fd08c",
+ "imageFormatVersion": "3.0",
+ "imageOptimization": "Default",
+ "bootupTimestamp": 1700729434.5892005,
+ "uptime": 20666.78,
+ "memTotal": 8099732,
+ "memFree": 4989568,
+ "isIntlVersion": False,
+ }
+ ],
},
),
"expected": {"is_online": True, "established": False, "hw_model": None},
@@ -507,8 +515,12 @@ REFRESH_DATA: list[dict[str, Any]] = [
{"return_value": True},
{
"side_effect": aioeapi.EapiCommandError(
- passed=[], failed="show version", errors=["Authorization denied for command 'show version'"], errmsg="Invalid command", not_exec=[]
- )
+ passed=[],
+ failed="show version",
+ errors=["Authorization denied for command 'show version'"],
+ errmsg="Invalid command",
+ not_exec=[],
+ ),
},
),
"expected": {"is_online": True, "established": False, "hw_model": None},
@@ -599,21 +611,17 @@ CACHE_STATS_DATA: list[ParameterSet] = [
class TestAntaDevice:
- """
- Test for anta.device.AntaDevice Abstract class
- """
+ """Test for anta.device.AntaDevice Abstract class."""
- @pytest.mark.asyncio
+ @pytest.mark.asyncio()
@pytest.mark.parametrize(
- "device, command_data, expected_data",
- map(lambda d: (d["device"], d["command"], d["expected"]), COLLECT_DATA),
+ ("device", "command_data", "expected_data"),
+ ((d["device"], d["command"], d["expected"]) for d in COLLECT_DATA),
indirect=["device"],
ids=generate_test_ids_list(COLLECT_DATA),
)
async def test_collect(self, device: AntaDevice, command_data: dict[str, Any], expected_data: dict[str, Any]) -> None:
- """
- Test AntaDevice.collect behavior
- """
+ """Test AntaDevice.collect behavior."""
command = AntaCommand(command=command_data["command"], use_cache=command_data["use_cache"])
# Dummy output for cache hit
@@ -646,32 +654,21 @@ class TestAntaDevice:
assert device.cache is None
device._collect.assert_called_once_with(command=command) # type: ignore[attr-defined] # pylint: disable=protected-access
- @pytest.mark.parametrize("device, expected", CACHE_STATS_DATA, indirect=["device"])
+ @pytest.mark.parametrize(("device", "expected"), CACHE_STATS_DATA, indirect=["device"])
def test_cache_statistics(self, device: AntaDevice, expected: dict[str, Any] | None) -> None:
- """
- Verify that when cache statistics attribute does not exist
- TODO add a test where cache has some value
- """
- assert device.cache_statistics == expected
+ """Verify that when cache statistics attribute does not exist.
- def test_supports(self, device: AntaDevice) -> None:
+ TODO add a test where cache has some value.
"""
- Test if the supports() method
- """
- command = AntaCommand(command="show hardware counter drop", errors=["Unavailable command (not supported on this hardware platform) (at token 2: 'counter')"])
- assert device.supports(command) is False
- command = AntaCommand(command="show hardware counter drop")
- assert device.supports(command) is True
+ assert device.cache_statistics == expected
class TestAsyncEOSDevice:
- """
- Test for anta.device.AsyncEOSDevice
- """
+ """Test for anta.device.AsyncEOSDevice."""
@pytest.mark.parametrize("data", INIT_DATA, ids=generate_test_ids_list(INIT_DATA))
def test__init__(self, data: dict[str, Any]) -> None:
- """Test the AsyncEOSDevice constructor"""
+ """Test the AsyncEOSDevice constructor."""
device = AsyncEOSDevice(**data["device"])
assert device.name == data["expected"]["name"]
@@ -683,12 +680,12 @@ class TestAsyncEOSDevice:
assert device.cache_locks is not None
hash(device)
- with patch("anta.device.__DEBUG__", True):
+ with patch("anta.device.__DEBUG__", new=True):
rprint(device)
@pytest.mark.parametrize("data", EQUALITY_DATA, ids=generate_test_ids_list(EQUALITY_DATA))
def test__eq(self, data: dict[str, Any]) -> None:
- """Test the AsyncEOSDevice equality"""
+ """Test the AsyncEOSDevice equality."""
device1 = AsyncEOSDevice(**data["device1"])
device2 = AsyncEOSDevice(**data["device2"])
if data["expected"]:
@@ -696,49 +693,45 @@ class TestAsyncEOSDevice:
else:
assert device1 != device2
- @pytest.mark.asyncio
+ @pytest.mark.asyncio()
@pytest.mark.parametrize(
- "async_device, patch_kwargs, expected",
- map(lambda d: (d["device"], d["patch_kwargs"], d["expected"]), REFRESH_DATA),
+ ("async_device", "patch_kwargs", "expected"),
+ ((d["device"], d["patch_kwargs"], d["expected"]) for d in REFRESH_DATA),
ids=generate_test_ids_list(REFRESH_DATA),
indirect=["async_device"],
)
async def test_refresh(self, async_device: AsyncEOSDevice, patch_kwargs: list[dict[str, Any]], expected: dict[str, Any]) -> None:
# pylint: disable=protected-access
- """Test AsyncEOSDevice.refresh()"""
- with patch.object(async_device._session, "check_connection", **patch_kwargs[0]):
- with patch.object(async_device._session, "cli", **patch_kwargs[1]):
- await async_device.refresh()
- async_device._session.check_connection.assert_called_once()
- if expected["is_online"]:
- async_device._session.cli.assert_called_once()
- assert async_device.is_online == expected["is_online"]
- assert async_device.established == expected["established"]
- assert async_device.hw_model == expected["hw_model"]
+ """Test AsyncEOSDevice.refresh()."""
+ with patch.object(async_device._session, "check_connection", **patch_kwargs[0]), patch.object(async_device._session, "cli", **patch_kwargs[1]):
+ await async_device.refresh()
+ async_device._session.check_connection.assert_called_once()
+ if expected["is_online"]:
+ async_device._session.cli.assert_called_once()
+ assert async_device.is_online == expected["is_online"]
+ assert async_device.established == expected["established"]
+ assert async_device.hw_model == expected["hw_model"]
- @pytest.mark.asyncio
+ @pytest.mark.asyncio()
@pytest.mark.parametrize(
- "async_device, command, expected",
- map(lambda d: (d["device"], d["command"], d["expected"]), AIOEAPI_COLLECT_DATA),
+ ("async_device", "command", "expected"),
+ ((d["device"], d["command"], d["expected"]) for d in AIOEAPI_COLLECT_DATA),
ids=generate_test_ids_list(AIOEAPI_COLLECT_DATA),
indirect=["async_device"],
)
async def test__collect(self, async_device: AsyncEOSDevice, command: dict[str, Any], expected: dict[str, Any]) -> None:
# pylint: disable=protected-access
- """Test AsyncEOSDevice._collect()"""
- if "revision" in command:
- cmd = AntaCommand(command=command["command"], revision=command["revision"])
- else:
- cmd = AntaCommand(command=command["command"])
+ """Test AsyncEOSDevice._collect()."""
+ cmd = AntaCommand(command=command["command"], revision=command["revision"]) if "revision" in command else AntaCommand(command=command["command"])
with patch.object(async_device._session, "cli", **command["patch_kwargs"]):
await async_device.collect(cmd)
- commands = []
+ commands: list[dict[str, Any]] = []
if async_device.enable and async_device._enable_password is not None:
commands.append(
{
"cmd": "enable",
"input": str(async_device._enable_password),
- }
+ },
)
elif async_device.enable:
# No password
@@ -751,15 +744,15 @@ class TestAsyncEOSDevice:
assert cmd.output == expected["output"]
assert cmd.errors == expected["errors"]
- @pytest.mark.asyncio
+ @pytest.mark.asyncio()
@pytest.mark.parametrize(
- "async_device, copy",
- map(lambda d: (d["device"], d["copy"]), AIOEAPI_COPY_DATA),
+ ("async_device", "copy"),
+ ((d["device"], d["copy"]) for d in AIOEAPI_COPY_DATA),
ids=generate_test_ids_list(AIOEAPI_COPY_DATA),
indirect=["async_device"],
)
async def test_copy(self, async_device: AsyncEOSDevice, copy: dict[str, Any]) -> None:
- """Test AsyncEOSDevice.copy()"""
+ """Test AsyncEOSDevice.copy()."""
conn = SSHClientConnection(asyncio.get_event_loop(), SSHClientConnectionOptions())
with patch("asyncssh.connect") as connect_mock:
connect_mock.return_value.__aenter__.return_value = conn
diff --git a/tests/units/test_logger.py b/tests/units/test_logger.py
index 6e1e5b4..d9b7c76 100644
--- a/tests/units/test_logger.py
+++ b/tests/units/test_logger.py
@@ -1,53 +1,65 @@
# 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.
-"""
-Tests for anta.logger
-"""
+"""Tests for anta.logger."""
+
from __future__ import annotations
import logging
-from typing import TYPE_CHECKING
from unittest.mock import patch
import pytest
-from anta.logger import anta_log_exception
-
-if TYPE_CHECKING:
- from pytest import LogCaptureFixture
+from anta.logger import anta_log_exception, exc_to_str, tb_to_str
@pytest.mark.parametrize(
- "exception, message, calling_logger, __DEBUG__value, expected_message",
+ ("exception", "message", "calling_logger", "debug_value", "expected_message"),
[
- pytest.param(ValueError("exception message"), None, None, False, "ValueError (exception message)", id="exception only"),
- pytest.param(ValueError("exception message"), "custom message", None, False, "custom message\nValueError (exception message)", id="custom message"),
+ pytest.param(
+ ValueError("exception message"),
+ None,
+ None,
+ False,
+ "ValueError: exception message",
+ id="exception only",
+ ),
+ pytest.param(
+ ValueError("exception message"),
+ "custom message",
+ None,
+ False,
+ "custom message\nValueError: exception message",
+ id="custom message",
+ ),
pytest.param(
ValueError("exception message"),
"custom logger",
logging.getLogger("custom"),
False,
- "custom logger\nValueError (exception message)",
+ "custom logger\nValueError: exception message",
id="custom logger",
),
pytest.param(
- ValueError("exception message"), "Use with custom message", None, True, "Use with custom message\nValueError (exception message)", id="__DEBUG__ on"
+ ValueError("exception message"),
+ "Use with custom message",
+ None,
+ True,
+ "Use with custom message\nValueError: exception message",
+ id="__DEBUG__ on",
),
],
)
def test_anta_log_exception(
- caplog: LogCaptureFixture,
+ caplog: pytest.LogCaptureFixture,
exception: Exception,
message: str | None,
calling_logger: logging.Logger | None,
- __DEBUG__value: bool,
+ debug_value: bool,
expected_message: str,
) -> None:
- """
- Test anta_log_exception
- """
-
+ # pylint: disable=too-many-arguments
+ """Test anta_log_exception."""
if calling_logger is not None:
# https://github.com/pytest-dev/pytest/issues/3697
calling_logger.propagate = True
@@ -57,12 +69,12 @@ def test_anta_log_exception(
# Need to raise to trigger nice stacktrace for __DEBUG__ == True
try:
raise exception
- except ValueError as e:
- with patch("anta.logger.__DEBUG__", __DEBUG__value):
- anta_log_exception(e, message=message, calling_logger=calling_logger)
+ except ValueError as exc:
+ with patch("anta.logger.__DEBUG__", new=debug_value):
+ anta_log_exception(exc, message=message, calling_logger=calling_logger)
# Two log captured
- if __DEBUG__value:
+ if debug_value:
assert len(caplog.record_tuples) == 2
else:
assert len(caplog.record_tuples) == 1
@@ -76,5 +88,29 @@ def test_anta_log_exception(
assert level == logging.CRITICAL
assert message == expected_message
# the only place where we can see the stracktrace is in the capture.text
- if __DEBUG__value is True:
+ if debug_value:
assert "Traceback" in caplog.text
+
+
+def my_raising_function(exception: Exception) -> None:
+ """Raise Exception."""
+ raise exception
+
+
+@pytest.mark.parametrize(
+ ("exception", "expected_output"),
+ [(ValueError("test"), "ValueError: test"), (ValueError(), "ValueError")],
+)
+def test_exc_to_str(exception: Exception, expected_output: str) -> None:
+ """Test exc_to_str."""
+ assert exc_to_str(exception) == expected_output
+
+
+def test_tb_to_str() -> None:
+ """Test tb_to_str."""
+ try:
+ my_raising_function(ValueError("test"))
+ except ValueError as exc:
+ output = tb_to_str(exc)
+ assert "Traceback" in output
+ assert 'my_raising_function(ValueError("test"))' in output
diff --git a/tests/units/test_models.py b/tests/units/test_models.py
index c0585a4..bc6a1ce 100644
--- a/tests/units/test_models.py
+++ b/tests/units/test_models.py
@@ -1,209 +1,242 @@
# 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
-"""
+"""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
+from typing import TYPE_CHECKING, Any, ClassVar
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
+if TYPE_CHECKING:
+ from anta.device import AntaDevice
+
class FakeTest(AntaTest):
- """ANTA test that always succeed"""
+ """ANTA test that always succeed."""
name = "FakeTest"
description = "ANTA test that always succeed"
- categories = []
- commands = []
+ categories: ClassVar[list[str]] = []
+ commands: ClassVar[list[AntaCommand | AntaTemplate]] = []
@AntaTest.anta_test
def test(self) -> None:
+ """Test function."""
self.result.is_success()
class FakeTestWithFailedCommand(AntaTest):
- """ANTA test with a command that failed"""
+ """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"])]
+ categories: ClassVar[list[str]] = []
+ commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show version", errors=["failed command"])]
@AntaTest.anta_test
def test(self) -> None:
+ """Test function."""
self.result.is_success()
class FakeTestWithUnsupportedCommand(AntaTest):
- """ANTA test with an unsupported command"""
+ """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')"])]
+ categories: ClassVar[list[str]] = []
+ commands: ClassVar[list[AntaCommand | AntaTemplate]] = [
+ 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:
+ """Test function."""
self.result.is_success()
class FakeTestWithInput(AntaTest):
- """ANTA test with inputs that always succeed"""
+ """ANTA test with inputs that always succeed."""
name = "FakeTestWithInput"
description = "ANTA test with inputs that always succeed"
- categories = []
- commands = []
+ categories: ClassVar[list[str]] = []
+ commands: ClassVar[list[AntaCommand | AntaTemplate]] = []
+
+ class Input(AntaTest.Input):
+ """Inputs for FakeTestWithInput test."""
- class Input(AntaTest.Input): # pylint: disable=missing-class-docstring
string: str
@AntaTest.anta_test
def test(self) -> None:
+ """Test function."""
self.result.is_success(self.inputs.string)
class FakeTestWithTemplate(AntaTest):
- """ANTA test with template that always succeed"""
+ """ANTA test with template that always succeed."""
name = "FakeTestWithTemplate"
description = "ANTA test with template that always succeed"
- categories = []
- commands = [AntaTemplate(template="show interface {interface}")]
+ categories: ClassVar[list[str]] = []
+ commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template="show interface {interface}")]
+
+ class Input(AntaTest.Input):
+ """Inputs for FakeTestWithTemplate test."""
- class Input(AntaTest.Input): # pylint: disable=missing-class-docstring
interface: str
def render(self, template: AntaTemplate) -> list[AntaCommand]:
+ """Render function."""
return [template.render(interface=self.inputs.interface)]
@AntaTest.anta_test
def test(self) -> None:
+ """Test function."""
self.result.is_success(self.instance_commands[0].command)
class FakeTestWithTemplateNoRender(AntaTest):
- """ANTA test with template that miss the render() method"""
+ """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}")]
+ categories: ClassVar[list[str]] = []
+ commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template="show interface {interface}")]
+
+ class Input(AntaTest.Input):
+ """Inputs for FakeTestWithTemplateNoRender test."""
- class Input(AntaTest.Input): # pylint: disable=missing-class-docstring
interface: str
@AntaTest.anta_test
def test(self) -> None:
+ """Test function."""
self.result.is_success(self.instance_commands[0].command)
class FakeTestWithTemplateBadRender1(AntaTest):
- """ANTA test with template that raises a AntaTemplateRenderError exception"""
+ """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}")]
+ categories: ClassVar[list[str]] = []
+ commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template="show interface {interface}")]
+
+ class Input(AntaTest.Input):
+ """Inputs for FakeTestWithTemplateBadRender1 test."""
- class Input(AntaTest.Input): # pylint: disable=missing-class-docstring
interface: str
def render(self, template: AntaTemplate) -> list[AntaCommand]:
+ """Render function."""
return [template.render(wrong_template_param=self.inputs.interface)]
@AntaTest.anta_test
def test(self) -> None:
+ """Test function."""
self.result.is_success(self.instance_commands[0].command)
class FakeTestWithTemplateBadRender2(AntaTest):
- """ANTA test with template that raises an arbitrary exception"""
+ """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}")]
+ categories: ClassVar[list[str]] = []
+ commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template="show interface {interface}")]
+
+ class Input(AntaTest.Input):
+ """Inputs for FakeTestWithTemplateBadRender2 test."""
- 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
+ """Render function."""
+ raise RuntimeError(template)
@AntaTest.anta_test
def test(self) -> None:
+ """Test function."""
self.result.is_success(self.instance_commands[0].command)
class SkipOnPlatformTest(AntaTest):
- """ANTA test that is skipped"""
+ """ANTA test that is skipped."""
name = "SkipOnPlatformTest"
description = "ANTA test that is skipped on a specific platform"
- categories = []
- commands = []
+ categories: ClassVar[list[str]] = []
+ commands: ClassVar[list[AntaCommand | AntaTemplate]] = []
@skip_on_platforms([DEVICE_HW_MODEL])
@AntaTest.anta_test
def test(self) -> None:
+ """Test function."""
self.result.is_success()
class UnSkipOnPlatformTest(AntaTest):
- """ANTA test that is skipped"""
+ """ANTA test that is skipped."""
name = "UnSkipOnPlatformTest"
description = "ANTA test that is skipped on a specific platform"
- categories = []
- commands = []
+ categories: ClassVar[list[str]] = []
+ commands: ClassVar[list[AntaCommand | AntaTemplate]] = []
@skip_on_platforms(["dummy"])
@AntaTest.anta_test
def test(self) -> None:
+ """Test function."""
self.result.is_success()
class SkipOnPlatformTestWithInput(AntaTest):
- """ANTA test skipped on platforms but with Input"""
+ """ANTA test skipped on platforms but with Input."""
name = "SkipOnPlatformTestWithInput"
description = "ANTA test skipped on platforms but with Input"
- categories = []
- commands = []
+ categories: ClassVar[list[str]] = []
+ commands: ClassVar[list[AntaCommand | AntaTemplate]] = []
+
+ class Input(AntaTest.Input):
+ """Inputs for SkipOnPlatformTestWithInput test."""
- class Input(AntaTest.Input): # pylint: disable=missing-class-docstring
string: str
@skip_on_platforms([DEVICE_HW_MODEL])
@AntaTest.anta_test
def test(self) -> None:
+ """Test function."""
self.result.is_success(self.inputs.string)
class DeprecatedTestWithoutNewTest(AntaTest):
- """ANTA test that is deprecated without new test"""
+ """ANTA test that is deprecated without new test."""
name = "DeprecatedTestWitouthNewTest"
description = "ANTA test that is deprecated without new test"
- categories = []
- commands = []
+ categories: ClassVar[list[str]] = []
+ commands: ClassVar[list[AntaCommand | AntaTemplate]] = []
@deprecated_test()
@AntaTest.anta_test
def test(self) -> None:
+ """Test function."""
self.result.is_success()
@@ -212,52 +245,88 @@ class DeprecatedTestWithNewTest(AntaTest):
name = "DeprecatedTestWithNewTest"
description = "ANTA deprecated test with New Test"
- categories = []
- commands = []
+ categories: ClassVar[list[str]] = []
+ commands: ClassVar[list[AntaCommand | AntaTemplate]] = []
@deprecated_test(new_tests=["NewTest"])
@AntaTest.anta_test
def test(self) -> None:
+ """Test function."""
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": "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"}},
+ "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"}},
+ "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"}},
+ "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"]}},
+ "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"]}},
+ "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"}},
+ "expected": {
+ "__init__": {
+ "result": "error",
+ "messages": ["Input should be a valid string"],
+ },
+ "test": {"result": "error"},
+ },
},
{
"name": "wrong render definition",
@@ -284,13 +353,13 @@ ANTATEST_DATA: list[dict[str, Any]] = [
},
},
{
- "name": "Exception in render()",
+ "name": "RuntimeError in render()",
"test": FakeTestWithTemplateBadRender2,
"inputs": {"interface": "Ethernet1"},
"expected": {
"__init__": {
"result": "error",
- "messages": ["Exception in tests.units.test_models.FakeTestWithTemplateBadRender2.render(): Exception"],
+ "messages": ["Exception in tests.units.test_models.FakeTestWithTemplateBadRender2.render(): RuntimeError"],
},
"test": {"result": "error"},
},
@@ -317,7 +386,10 @@ ANTATEST_DATA: list[dict[str, Any]] = [
"name": "skip on platforms, not unset",
"test": SkipOnPlatformTestWithInput,
"inputs": None,
- "expected": {"__init__": {"result": "error", "messages": ["Field required"]}, "test": {"result": "error"}},
+ "expected": {
+ "__init__": {"result": "error", "messages": ["Field required"]},
+ "test": {"result": "error"},
+ },
},
{
"name": "deprecate test without new test",
@@ -341,7 +413,13 @@ ANTATEST_DATA: list[dict[str, Any]] = [
"name": "failed command",
"test": FakeTestWithFailedCommand,
"inputs": None,
- "expected": {"__init__": {"result": "unset"}, "test": {"result": "error", "messages": ["show version has failed: failed command"]}},
+ "expected": {
+ "__init__": {"result": "unset"},
+ "test": {
+ "result": "error",
+ "messages": ["show version has failed: failed command"],
+ },
+ },
},
{
"name": "unsupported command",
@@ -349,29 +427,30 @@ ANTATEST_DATA: list[dict[str, Any]] = [
"inputs": None,
"expected": {
"__init__": {"result": "unset"},
- "test": {"result": "skipped", "messages": ["Skipped because show hardware counter drop is not supported on pytest"]},
+ "test": {
+ "result": "skipped",
+ "messages": ["'show hardware counter drop' is not supported on pytest"],
+ },
},
},
]
-class Test_AntaTest:
- """
- Test for anta.models.AntaTest
- """
+class TestAntaTest:
+ """Test for anta.models.AntaTest."""
def test__init_subclass__name(self) -> None:
- """Test __init_subclass__"""
+ """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"""
+ """ANTA test that is missing a name."""
description = "ANTA test that is missing a name"
- categories = []
- commands = []
+ categories: ClassVar[list[str]] = []
+ commands: ClassVar[list[AntaCommand | AntaTemplate]] = []
@AntaTest.anta_test
def test(self) -> None:
@@ -382,11 +461,11 @@ class Test_AntaTest:
with pytest.raises(NotImplementedError) as exec_info:
class WrongTestNoDescription(AntaTest):
- """ANTA test that is missing a description"""
+ """ANTA test that is missing a description."""
name = "WrongTestNoDescription"
- categories = []
- commands = []
+ categories: ClassVar[list[str]] = []
+ commands: ClassVar[list[AntaCommand | AntaTemplate]] = []
@AntaTest.anta_test
def test(self) -> None:
@@ -397,11 +476,11 @@ class Test_AntaTest:
with pytest.raises(NotImplementedError) as exec_info:
class WrongTestNoCategories(AntaTest):
- """ANTA test that is missing categories"""
+ """ANTA test that is missing categories."""
name = "WrongTestNoCategories"
description = "ANTA test that is missing categories"
- commands = []
+ commands: ClassVar[list[AntaCommand | AntaTemplate]] = []
@AntaTest.anta_test
def test(self) -> None:
@@ -412,11 +491,11 @@ class Test_AntaTest:
with pytest.raises(NotImplementedError) as exec_info:
class WrongTestNoCommands(AntaTest):
- """ANTA test that is missing commands"""
+ """ANTA test that is missing commands."""
name = "WrongTestNoCommands"
description = "ANTA test that is missing commands"
- categories = []
+ categories: ClassVar[list[str]] = []
@AntaTest.anta_test
def test(self) -> None:
@@ -432,14 +511,14 @@ class Test_AntaTest:
@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"""
+ """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"""
+ """Test the AntaTest.test method."""
expected = data["expected"]["test"]
test = data["test"](device, inputs=data["inputs"])
asyncio.run(test.test())
@@ -454,12 +533,12 @@ def test_blacklist(device: AntaDevice, data: str) -> None:
"""Test for blacklisting function."""
class FakeTestWithBlacklist(AntaTest):
- """Fake Test for blacklist"""
+ """Fake Test for blacklist."""
name = "FakeTestWithBlacklist"
description = "ANTA test that has blacklisted command"
- categories = []
- commands = [AntaCommand(command=data)]
+ categories: ClassVar[list[str]] = []
+ commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=data)]
@AntaTest.anta_test
def test(self) -> None:
@@ -470,3 +549,61 @@ def test_blacklist(device: AntaDevice, data: str) -> None:
# Run the test() method
asyncio.run(test_instance.test())
assert test_instance.result.result == "error"
+
+
+class TestAntaComamnd:
+ """Test for anta.models.AntaCommand."""
+
+ # ruff: noqa: B018
+ # pylint: disable=pointless-statement
+
+ def test_empty_output_access(self) -> None:
+ """Test for both json and text ofmt."""
+ json_cmd = AntaCommand(command="show dummy")
+ text_cmd = AntaCommand(command="show dummy", ofmt="text")
+ msg = "There is no output for command 'show dummy'"
+ with pytest.raises(RuntimeError, match=msg):
+ json_cmd.json_output
+ with pytest.raises(RuntimeError, match=msg):
+ text_cmd.text_output
+
+ def test_wrong_format_output_access(self) -> None:
+ """Test for both json and text ofmt."""
+ json_cmd = AntaCommand(command="show dummy", output={})
+ json_cmd_2 = AntaCommand(command="show dummy", output="not_json")
+ text_cmd = AntaCommand(command="show dummy", ofmt="text", output="blah")
+ text_cmd_2 = AntaCommand(command="show dummy", ofmt="text", output={"not_a": "string"})
+ msg = "Output of command 'show dummy' is invalid"
+ msg = "Output of command 'show dummy' is invalid"
+ with pytest.raises(RuntimeError, match=msg):
+ json_cmd.text_output
+ with pytest.raises(RuntimeError, match=msg):
+ text_cmd.json_output
+ with pytest.raises(RuntimeError, match=msg):
+ json_cmd_2.text_output
+ with pytest.raises(RuntimeError, match=msg):
+ text_cmd_2.json_output
+
+ def test_supported(self) -> None:
+ """Test if the supported property."""
+ command = AntaCommand(command="show hardware counter drop", errors=["Unavailable command (not supported on this hardware platform) (at token 2: 'counter')"])
+ assert command.supported is False
+ command = AntaCommand(
+ command="show hardware counter drop", output={"totalAdverseDrops": 0, "totalCongestionDrops": 0, "totalPacketProcessorDrops": 0, "dropEvents": {}}
+ )
+ assert command.supported is True
+
+ def test_requires_privileges(self) -> None:
+ """Test if the requires_privileges property."""
+ command = AntaCommand(command="show aaa methods accounting", errors=["Invalid input (privileged mode required)"])
+ assert command.requires_privileges is True
+ command = AntaCommand(
+ command="show aaa methods accounting",
+ output={
+ "commandsAcctMethods": {"privilege0-15": {"defaultMethods": [], "consoleMethods": []}},
+ "execAcctMethods": {"exec": {"defaultMethods": [], "consoleMethods": []}},
+ "systemAcctMethods": {"system": {"defaultMethods": [], "consoleMethods": []}},
+ "dot1xAcctMethods": {"dot1x": {"defaultMethods": [], "consoleMethods": []}},
+ },
+ )
+ assert command.requires_privileges is False
diff --git a/tests/units/test_runner.py b/tests/units/test_runner.py
index c353cbe..d5bb892 100644
--- a/tests/units/test_runner.py
+++ b/tests/units/test_runner.py
@@ -1,13 +1,11 @@
# 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.runner.py
-"""
+"""test anta.runner.py."""
+
from __future__ import annotations
import logging
-from typing import TYPE_CHECKING
import pytest
@@ -19,16 +17,12 @@ from anta.runner import main
from .test_models import FakeTest
-if TYPE_CHECKING:
- from pytest import LogCaptureFixture
-
FAKE_CATALOG: AntaCatalog = AntaCatalog.from_list([(FakeTest, None)])
-@pytest.mark.asyncio
-async def test_runner_empty_tests(caplog: LogCaptureFixture, test_inventory: AntaInventory) -> None:
- """
- Test that when the list of tests is empty, a log is raised
+@pytest.mark.asyncio()
+async def test_runner_empty_tests(caplog: pytest.LogCaptureFixture, test_inventory: AntaInventory) -> None:
+ """Test that when the list of tests is empty, a log is raised.
caplog is the pytest fixture to capture logs
test_inventory is a fixture that gives a default inventory for tests
@@ -42,10 +36,9 @@ async def test_runner_empty_tests(caplog: LogCaptureFixture, test_inventory: Ant
assert "The list of tests is empty, exiting" in caplog.records[0].message
-@pytest.mark.asyncio
-async def test_runner_empty_inventory(caplog: LogCaptureFixture) -> None:
- """
- Test that when the Inventory is empty, a log is raised
+@pytest.mark.asyncio()
+async def test_runner_empty_inventory(caplog: pytest.LogCaptureFixture) -> None:
+ """Test that when the Inventory is empty, a log is raised.
caplog is the pytest fixture to capture logs
"""
@@ -58,10 +51,9 @@ async def test_runner_empty_inventory(caplog: LogCaptureFixture) -> None:
assert "The inventory is empty, exiting" in caplog.records[0].message
-@pytest.mark.asyncio
-async def test_runner_no_selected_device(caplog: LogCaptureFixture, test_inventory: AntaInventory) -> None:
- """
- Test that when the list of established device
+@pytest.mark.asyncio()
+async def test_runner_no_selected_device(caplog: pytest.LogCaptureFixture, test_inventory: AntaInventory) -> None:
+ """Test that when the list of established device.
caplog is the pytest fixture to capture logs
test_inventory is a fixture that gives a default inventory for tests
@@ -71,12 +63,10 @@ async def test_runner_no_selected_device(caplog: LogCaptureFixture, test_invento
manager = ResultManager()
await main(manager, test_inventory, FAKE_CATALOG)
- assert "No device in the established state 'True' was found. There is no device to run tests against, exiting" in [record.message for record in caplog.records]
+ assert "No reachable device was found." in [record.message for record in caplog.records]
# Reset logs and run with tags
caplog.clear()
- await main(manager, test_inventory, FAKE_CATALOG, tags=["toto"])
+ await main(manager, test_inventory, FAKE_CATALOG, tags={"toto"})
- assert "No device in the established state 'True' matching the tags ['toto'] was found. There is no device to run tests against, exiting" in [
- record.message for record in caplog.records
- ]
+ assert "No reachable device matching the tags {'toto'} was found." in [record.message for record in caplog.records]
diff --git a/tests/units/test_tools.py b/tests/units/test_tools.py
new file mode 100644
index 0000000..a846fd6
--- /dev/null
+++ b/tests/units/test_tools.py
@@ -0,0 +1,490 @@
+# 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.
+"""Tests for `anta.tools`."""
+
+from __future__ import annotations
+
+from contextlib import AbstractContextManager
+from contextlib import nullcontext as does_not_raise
+from typing import Any
+
+import pytest
+
+from anta.tools import get_dict_superset, get_failed_logs, get_item, get_value
+
+TEST_GET_FAILED_LOGS_DATA = [
+ {"id": 1, "name": "Alice", "age": 30, "email": "alice@example.com"},
+ {"id": 2, "name": "Bob", "age": 35, "email": "bob@example.com"},
+ {"id": 3, "name": "Charlie", "age": 40, "email": "charlie@example.com"},
+ {"id": 4, "name": "Jon", "age": 25, "email": "Jon@example.com"},
+ {"id": 4, "name": "Rob", "age": 25, "email": "Jon@example.com"},
+]
+TEST_GET_DICT_SUPERSET_DATA = [
+ ("id", 0),
+ {
+ "id": 1,
+ "name": "Alice",
+ "age": 30,
+ "email": "alice@example.com",
+ },
+ {
+ "id": 2,
+ "name": "Bob",
+ "age": 35,
+ "email": "bob@example.com",
+ },
+ {
+ "id": 3,
+ "name": "Charlie",
+ "age": 40,
+ "email": "charlie@example.com",
+ },
+]
+TEST_GET_VALUE_DATA = {"test_value": 42, "nested_test": {"nested_value": 43}}
+TEST_GET_ITEM_DATA = [
+ ("id", 0),
+ {
+ "id": 1,
+ "name": "Alice",
+ "age": 30,
+ "email": "alice@example.com",
+ },
+ {
+ "id": 2,
+ "name": "Bob",
+ "age": 35,
+ "email": "bob@example.com",
+ },
+ {
+ "id": 3,
+ "name": "Charlie",
+ "age": 40,
+ "email": "charlie@example.com",
+ },
+]
+
+
+@pytest.mark.parametrize(
+ ("expected_output", "actual_output", "expected_result"),
+ [
+ pytest.param(
+ TEST_GET_FAILED_LOGS_DATA[0],
+ TEST_GET_FAILED_LOGS_DATA[0],
+ "",
+ id="no difference",
+ ),
+ pytest.param(
+ TEST_GET_FAILED_LOGS_DATA[0],
+ TEST_GET_FAILED_LOGS_DATA[1],
+ "\nExpected `1` as the id, but found `2` instead.\nExpected `Alice` as the name, but found `Bob` instead.\n"
+ "Expected `30` as the age, but found `35` instead.\nExpected `alice@example.com` as the email, but found `bob@example.com` instead.",
+ id="different data",
+ ),
+ pytest.param(
+ TEST_GET_FAILED_LOGS_DATA[0],
+ {},
+ "\nExpected `1` as the id, but it was not found in the actual output.\nExpected `Alice` as the name, but it was not found in the actual output.\n"
+ "Expected `30` as the age, but it was not found in the actual output.\nExpected `alice@example.com` as the email, but it was not found in "
+ "the actual output.",
+ id="empty actual output",
+ ),
+ pytest.param(
+ TEST_GET_FAILED_LOGS_DATA[3],
+ TEST_GET_FAILED_LOGS_DATA[4],
+ "\nExpected `Jon` as the name, but found `Rob` instead.",
+ id="different name",
+ ),
+ ],
+)
+def test_get_failed_logs(
+ expected_output: dict[Any, Any],
+ actual_output: dict[Any, Any],
+ expected_result: str,
+) -> None:
+ """Test get_failed_logs."""
+ assert get_failed_logs(expected_output, actual_output) == expected_result
+
+
+@pytest.mark.parametrize(
+ (
+ "list_of_dicts",
+ "input_dict",
+ "default",
+ "required",
+ "var_name",
+ "custom_error_msg",
+ "expected_result",
+ "expected_raise",
+ ),
+ [
+ pytest.param(
+ [],
+ {"id": 1, "name": "Alice"},
+ None,
+ False,
+ None,
+ None,
+ None,
+ does_not_raise(),
+ id="empty list",
+ ),
+ pytest.param(
+ [],
+ {"id": 1, "name": "Alice"},
+ None,
+ True,
+ None,
+ None,
+ None,
+ pytest.raises(ValueError, match="not found in the provided list."),
+ id="empty list and required",
+ ),
+ pytest.param(
+ TEST_GET_DICT_SUPERSET_DATA,
+ {"id": 10, "name": "Jack"},
+ None,
+ False,
+ None,
+ None,
+ None,
+ does_not_raise(),
+ id="missing item",
+ ),
+ pytest.param(
+ TEST_GET_DICT_SUPERSET_DATA,
+ {"id": 1, "name": "Alice"},
+ None,
+ False,
+ None,
+ None,
+ TEST_GET_DICT_SUPERSET_DATA[1],
+ does_not_raise(),
+ id="found item",
+ ),
+ pytest.param(
+ TEST_GET_DICT_SUPERSET_DATA,
+ {"id": 10, "name": "Jack"},
+ "default_value",
+ False,
+ None,
+ None,
+ "default_value",
+ does_not_raise(),
+ id="default value",
+ ),
+ pytest.param(
+ TEST_GET_DICT_SUPERSET_DATA,
+ {"id": 10, "name": "Jack"},
+ None,
+ True,
+ None,
+ None,
+ None,
+ pytest.raises(ValueError, match="not found in the provided list."),
+ id="required",
+ ),
+ pytest.param(
+ TEST_GET_DICT_SUPERSET_DATA,
+ {"id": 10, "name": "Jack"},
+ None,
+ True,
+ "custom_var_name",
+ None,
+ None,
+ pytest.raises(ValueError, match="custom_var_name not found in the provided list."),
+ id="custom var_name",
+ ),
+ pytest.param(
+ TEST_GET_DICT_SUPERSET_DATA,
+ {"id": 1, "name": "Alice"},
+ None,
+ True,
+ "custom_var_name",
+ "Custom error message",
+ TEST_GET_DICT_SUPERSET_DATA[1],
+ does_not_raise(),
+ id="custom error message",
+ ),
+ pytest.param(
+ TEST_GET_DICT_SUPERSET_DATA,
+ {"id": 10, "name": "Jack"},
+ None,
+ True,
+ "custom_var_name",
+ "Custom error message",
+ None,
+ pytest.raises(ValueError, match="Custom error message"),
+ id="custom error message and required",
+ ),
+ pytest.param(
+ TEST_GET_DICT_SUPERSET_DATA,
+ {"id": 1, "name": "Jack"},
+ None,
+ False,
+ None,
+ None,
+ None,
+ does_not_raise(),
+ id="id ok but name not ok",
+ ),
+ pytest.param(
+ "not a list",
+ {"id": 1, "name": "Alice"},
+ None,
+ True,
+ None,
+ None,
+ None,
+ pytest.raises(ValueError, match="not found in the provided list."),
+ id="non-list input for list_of_dicts",
+ ),
+ pytest.param(
+ TEST_GET_DICT_SUPERSET_DATA,
+ "not a dict",
+ None,
+ True,
+ None,
+ None,
+ None,
+ pytest.raises(ValueError, match="not found in the provided list."),
+ id="non-dictionary input",
+ ),
+ pytest.param(
+ TEST_GET_DICT_SUPERSET_DATA,
+ {},
+ None,
+ False,
+ None,
+ None,
+ None,
+ does_not_raise(),
+ id="empty dictionary input",
+ ),
+ pytest.param(
+ TEST_GET_DICT_SUPERSET_DATA,
+ {"id": 1, "name": "Alice", "extra_key": "extra_value"},
+ None,
+ True,
+ None,
+ None,
+ None,
+ pytest.raises(ValueError, match="not found in the provided list."),
+ id="input dictionary with extra keys",
+ ),
+ pytest.param(
+ TEST_GET_DICT_SUPERSET_DATA,
+ {"id": 1},
+ None,
+ False,
+ None,
+ None,
+ TEST_GET_DICT_SUPERSET_DATA[1],
+ does_not_raise(),
+ id="input dictionary is a subset of more than one dictionary in list_of_dicts",
+ ),
+ pytest.param(
+ TEST_GET_DICT_SUPERSET_DATA,
+ {
+ "id": 1,
+ "name": "Alice",
+ "age": 30,
+ "email": "alice@example.com",
+ "extra_key": "extra_value",
+ },
+ None,
+ True,
+ None,
+ None,
+ None,
+ pytest.raises(ValueError, match="not found in the provided list."),
+ id="input dictionary is a superset of a dictionary in list_of_dicts",
+ ),
+ ],
+)
+def test_get_dict_superset(
+ list_of_dicts: list[dict[Any, Any]],
+ input_dict: dict[Any, Any],
+ default: str | None,
+ required: bool,
+ var_name: str | None,
+ custom_error_msg: str | None,
+ expected_result: str,
+ expected_raise: AbstractContextManager[Exception],
+) -> None:
+ """Test get_dict_superset."""
+ # pylint: disable=too-many-arguments
+ with expected_raise:
+ assert get_dict_superset(list_of_dicts, input_dict, default, var_name, custom_error_msg, required=required) == expected_result
+
+
+@pytest.mark.parametrize(
+ (
+ "input_dict",
+ "key",
+ "default",
+ "required",
+ "org_key",
+ "separator",
+ "expected_result",
+ "expected_raise",
+ ),
+ [
+ pytest.param({}, "test", None, False, None, None, None, does_not_raise(), id="empty dict"),
+ pytest.param(
+ TEST_GET_VALUE_DATA,
+ "test_value",
+ None,
+ False,
+ None,
+ None,
+ 42,
+ does_not_raise(),
+ id="simple key",
+ ),
+ pytest.param(
+ TEST_GET_VALUE_DATA,
+ "nested_test.nested_value",
+ None,
+ False,
+ None,
+ None,
+ 43,
+ does_not_raise(),
+ id="nested_key",
+ ),
+ pytest.param(
+ TEST_GET_VALUE_DATA,
+ "missing_value",
+ None,
+ False,
+ None,
+ None,
+ None,
+ does_not_raise(),
+ id="missing_value",
+ ),
+ pytest.param(
+ TEST_GET_VALUE_DATA,
+ "missing_value_with_default",
+ "default_value",
+ False,
+ None,
+ None,
+ "default_value",
+ does_not_raise(),
+ id="default",
+ ),
+ pytest.param(
+ TEST_GET_VALUE_DATA,
+ "missing_required",
+ None,
+ True,
+ None,
+ None,
+ None,
+ pytest.raises(ValueError, match="missing_required"),
+ id="required",
+ ),
+ pytest.param(
+ TEST_GET_VALUE_DATA,
+ "missing_required",
+ None,
+ True,
+ "custom_org_key",
+ None,
+ None,
+ pytest.raises(ValueError, match="custom_org_key"),
+ id="custom org_key",
+ ),
+ pytest.param(
+ TEST_GET_VALUE_DATA,
+ "nested_test||nested_value",
+ None,
+ None,
+ None,
+ "||",
+ 43,
+ does_not_raise(),
+ id="custom separator",
+ ),
+ ],
+)
+def test_get_value(
+ input_dict: dict[Any, Any],
+ key: str,
+ default: str | None,
+ required: bool,
+ org_key: str | None,
+ separator: str | None,
+ expected_result: int | str | None,
+ expected_raise: AbstractContextManager[Exception],
+) -> None:
+ """Test get_value."""
+ # pylint: disable=too-many-arguments
+ kwargs = {
+ "default": default,
+ "required": required,
+ "org_key": org_key,
+ "separator": separator,
+ }
+ kwargs = {k: v for k, v in kwargs.items() if v is not None}
+ with expected_raise:
+ assert get_value(input_dict, key, **kwargs) == expected_result # type: ignore[arg-type]
+
+
+@pytest.mark.parametrize(
+ ("list_of_dicts", "key", "value", "default", "required", "case_sensitive", "var_name", "custom_error_msg", "expected_result", "expected_raise"),
+ [
+ pytest.param([], "name", "Bob", None, False, False, None, None, None, does_not_raise(), id="empty list"),
+ pytest.param([], "name", "Bob", None, True, False, None, None, None, pytest.raises(ValueError, match="name"), id="empty list and required"),
+ pytest.param(TEST_GET_ITEM_DATA, "name", "Jack", None, False, False, None, None, None, does_not_raise(), id="missing item"),
+ pytest.param(TEST_GET_ITEM_DATA, "name", "Alice", None, False, False, None, None, TEST_GET_ITEM_DATA[1], does_not_raise(), id="found item"),
+ pytest.param(TEST_GET_ITEM_DATA, "name", "Jack", "default_value", False, False, None, None, "default_value", does_not_raise(), id="default value"),
+ pytest.param(TEST_GET_ITEM_DATA, "name", "Jack", None, True, False, None, None, None, pytest.raises(ValueError, match="name"), id="required"),
+ pytest.param(TEST_GET_ITEM_DATA, "name", "Bob", None, False, True, None, None, TEST_GET_ITEM_DATA[2], does_not_raise(), id="case sensitive"),
+ pytest.param(TEST_GET_ITEM_DATA, "name", "charlie", None, False, False, None, None, TEST_GET_ITEM_DATA[3], does_not_raise(), id="case insensitive"),
+ pytest.param(
+ TEST_GET_ITEM_DATA,
+ "name",
+ "Jack",
+ None,
+ True,
+ False,
+ "custom_var_name",
+ None,
+ None,
+ pytest.raises(ValueError, match="custom_var_name"),
+ id="custom var_name",
+ ),
+ pytest.param(
+ TEST_GET_ITEM_DATA,
+ "name",
+ "Jack",
+ None,
+ True,
+ False,
+ None,
+ "custom_error_msg",
+ None,
+ pytest.raises(ValueError, match="custom_error_msg"),
+ id="custom error msg",
+ ),
+ ],
+)
+def test_get_item(
+ list_of_dicts: list[dict[Any, Any]],
+ key: str,
+ value: str | None,
+ default: str | None,
+ required: bool,
+ case_sensitive: bool,
+ var_name: str | None,
+ custom_error_msg: str | None,
+ expected_result: str,
+ expected_raise: AbstractContextManager[Exception],
+) -> None:
+ """Test get_item."""
+ # pylint: disable=too-many-arguments
+ with expected_raise:
+ assert get_item(list_of_dicts, key, value, default, var_name, custom_error_msg, required=required, case_sensitive=case_sensitive) == expected_result
diff --git a/tests/units/tools/__init__.py b/tests/units/tools/__init__.py
deleted file mode 100644
index e772bee..0000000
--- a/tests/units/tools/__init__.py
+++ /dev/null
@@ -1,3 +0,0 @@
-# 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.
diff --git a/tests/units/tools/test_get_dict_superset.py b/tests/units/tools/test_get_dict_superset.py
deleted file mode 100644
index 63e08b5..0000000
--- a/tests/units/tools/test_get_dict_superset.py
+++ /dev/null
@@ -1,149 +0,0 @@
-# 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.
-
-"""Tests for `anta.tools.get_dict_superset`."""
-from __future__ import annotations
-
-from contextlib import nullcontext as does_not_raise
-from typing import Any
-
-import pytest
-
-from anta.tools.get_dict_superset import get_dict_superset
-
-# pylint: disable=duplicate-code
-DUMMY_DATA = [
- ("id", 0),
- {
- "id": 1,
- "name": "Alice",
- "age": 30,
- "email": "alice@example.com",
- },
- {
- "id": 2,
- "name": "Bob",
- "age": 35,
- "email": "bob@example.com",
- },
- {
- "id": 3,
- "name": "Charlie",
- "age": 40,
- "email": "charlie@example.com",
- },
-]
-
-
-@pytest.mark.parametrize(
- "list_of_dicts, input_dict, default, required, var_name, custom_error_msg, expected_result, expected_raise",
- [
- pytest.param([], {"id": 1, "name": "Alice"}, None, False, None, None, None, does_not_raise(), id="empty list"),
- pytest.param(
- [],
- {"id": 1, "name": "Alice"},
- None,
- True,
- None,
- None,
- None,
- pytest.raises(ValueError, match="not found in the provided list."),
- id="empty list and required",
- ),
- pytest.param(DUMMY_DATA, {"id": 10, "name": "Jack"}, None, False, None, None, None, does_not_raise(), id="missing item"),
- pytest.param(DUMMY_DATA, {"id": 1, "name": "Alice"}, None, False, None, None, DUMMY_DATA[1], does_not_raise(), id="found item"),
- pytest.param(DUMMY_DATA, {"id": 10, "name": "Jack"}, "default_value", False, None, None, "default_value", does_not_raise(), id="default value"),
- pytest.param(
- DUMMY_DATA, {"id": 10, "name": "Jack"}, None, True, None, None, None, pytest.raises(ValueError, match="not found in the provided list."), id="required"
- ),
- pytest.param(
- DUMMY_DATA,
- {"id": 10, "name": "Jack"},
- None,
- True,
- "custom_var_name",
- None,
- None,
- pytest.raises(ValueError, match="custom_var_name not found in the provided list."),
- id="custom var_name",
- ),
- pytest.param(
- DUMMY_DATA, {"id": 1, "name": "Alice"}, None, True, "custom_var_name", "Custom error message", DUMMY_DATA[1], does_not_raise(), id="custom error message"
- ),
- pytest.param(
- DUMMY_DATA,
- {"id": 10, "name": "Jack"},
- None,
- True,
- "custom_var_name",
- "Custom error message",
- None,
- pytest.raises(ValueError, match="Custom error message"),
- id="custom error message and required",
- ),
- pytest.param(DUMMY_DATA, {"id": 1, "name": "Jack"}, None, False, None, None, None, does_not_raise(), id="id ok but name not ok"),
- pytest.param(
- "not a list",
- {"id": 1, "name": "Alice"},
- None,
- True,
- None,
- None,
- None,
- pytest.raises(ValueError, match="not found in the provided list."),
- id="non-list input for list_of_dicts",
- ),
- pytest.param(
- DUMMY_DATA, "not a dict", None, True, None, None, None, pytest.raises(ValueError, match="not found in the provided list."), id="non-dictionary input"
- ),
- pytest.param(DUMMY_DATA, {}, None, False, None, None, None, does_not_raise(), id="empty dictionary input"),
- pytest.param(
- DUMMY_DATA,
- {"id": 1, "name": "Alice", "extra_key": "extra_value"},
- None,
- True,
- None,
- None,
- None,
- pytest.raises(ValueError, match="not found in the provided list."),
- id="input dictionary with extra keys",
- ),
- pytest.param(
- DUMMY_DATA,
- {"id": 1},
- None,
- False,
- None,
- None,
- DUMMY_DATA[1],
- does_not_raise(),
- id="input dictionary is a subset of more than one dictionary in list_of_dicts",
- ),
- pytest.param(
- DUMMY_DATA,
- {"id": 1, "name": "Alice", "age": 30, "email": "alice@example.com", "extra_key": "extra_value"},
- None,
- True,
- None,
- None,
- None,
- pytest.raises(ValueError, match="not found in the provided list."),
- id="input dictionary is a superset of a dictionary in list_of_dicts",
- ),
- ],
-)
-def test_get_dict_superset(
- list_of_dicts: list[dict[Any, Any]],
- input_dict: Any,
- default: Any | None,
- required: bool,
- var_name: str | None,
- custom_error_msg: str | None,
- expected_result: str,
- expected_raise: Any,
-) -> None:
- """Test get_dict_superset."""
- # pylint: disable=too-many-arguments
- with expected_raise:
- assert get_dict_superset(list_of_dicts, input_dict, default, required, var_name, custom_error_msg) == expected_result
diff --git a/tests/units/tools/test_get_item.py b/tests/units/tools/test_get_item.py
deleted file mode 100644
index 7d75e9c..0000000
--- a/tests/units/tools/test_get_item.py
+++ /dev/null
@@ -1,72 +0,0 @@
-# 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.
-
-"""Tests for `anta.tools.get_item`."""
-from __future__ import annotations
-
-from contextlib import nullcontext as does_not_raise
-from typing import Any
-
-import pytest
-
-from anta.tools.get_item import get_item
-
-DUMMY_DATA = [
- ("id", 0),
- {
- "id": 1,
- "name": "Alice",
- "age": 30,
- "email": "alice@example.com",
- },
- {
- "id": 2,
- "name": "Bob",
- "age": 35,
- "email": "bob@example.com",
- },
- {
- "id": 3,
- "name": "Charlie",
- "age": 40,
- "email": "charlie@example.com",
- },
-]
-
-
-@pytest.mark.parametrize(
- "list_of_dicts, key, value, default, required, case_sensitive, var_name, custom_error_msg, expected_result, expected_raise",
- [
- pytest.param([], "name", "Bob", None, False, False, None, None, None, does_not_raise(), id="empty list"),
- pytest.param([], "name", "Bob", None, True, False, None, None, None, pytest.raises(ValueError, match="name"), id="empty list and required"),
- pytest.param(DUMMY_DATA, "name", "Jack", None, False, False, None, None, None, does_not_raise(), id="missing item"),
- pytest.param(DUMMY_DATA, "name", "Alice", None, False, False, None, None, DUMMY_DATA[1], does_not_raise(), id="found item"),
- pytest.param(DUMMY_DATA, "name", "Jack", "default_value", False, False, None, None, "default_value", does_not_raise(), id="default value"),
- pytest.param(DUMMY_DATA, "name", "Jack", None, True, False, None, None, None, pytest.raises(ValueError, match="name"), id="required"),
- pytest.param(DUMMY_DATA, "name", "Bob", None, False, True, None, None, DUMMY_DATA[2], does_not_raise(), id="case sensitive"),
- pytest.param(DUMMY_DATA, "name", "charlie", None, False, False, None, None, DUMMY_DATA[3], does_not_raise(), id="case insensitive"),
- pytest.param(
- DUMMY_DATA, "name", "Jack", None, True, False, "custom_var_name", None, None, pytest.raises(ValueError, match="custom_var_name"), id="custom var_name"
- ),
- pytest.param(
- DUMMY_DATA, "name", "Jack", None, True, False, None, "custom_error_msg", None, pytest.raises(ValueError, match="custom_error_msg"), id="custom error msg"
- ),
- ],
-)
-def test_get_item(
- list_of_dicts: list[dict[Any, Any]],
- key: Any,
- value: Any,
- default: Any | None,
- required: bool,
- case_sensitive: bool,
- var_name: str | None,
- custom_error_msg: str | None,
- expected_result: str,
- expected_raise: Any,
-) -> None:
- """Test get_item."""
- # pylint: disable=too-many-arguments
- with expected_raise:
- assert get_item(list_of_dicts, key, value, default, required, case_sensitive, var_name, custom_error_msg) == expected_result
diff --git a/tests/units/tools/test_get_value.py b/tests/units/tools/test_get_value.py
deleted file mode 100644
index 73344d1..0000000
--- a/tests/units/tools/test_get_value.py
+++ /dev/null
@@ -1,50 +0,0 @@
-# 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.
-"""
-Tests for anta.tools.get_value
-"""
-
-from __future__ import annotations
-
-from contextlib import nullcontext as does_not_raise
-from typing import Any
-
-import pytest
-
-from anta.tools.get_value import get_value
-
-INPUT_DICT = {"test_value": 42, "nested_test": {"nested_value": 43}}
-
-
-@pytest.mark.parametrize(
- "input_dict, key, default, required, org_key, separator, expected_result, expected_raise",
- [
- pytest.param({}, "test", None, False, None, None, None, does_not_raise(), id="empty dict"),
- pytest.param(INPUT_DICT, "test_value", None, False, None, None, 42, does_not_raise(), id="simple key"),
- pytest.param(INPUT_DICT, "nested_test.nested_value", None, False, None, None, 43, does_not_raise(), id="nested_key"),
- pytest.param(INPUT_DICT, "missing_value", None, False, None, None, None, does_not_raise(), id="missing_value"),
- pytest.param(INPUT_DICT, "missing_value_with_default", "default_value", False, None, None, "default_value", does_not_raise(), id="default"),
- pytest.param(INPUT_DICT, "missing_required", None, True, None, None, None, pytest.raises(ValueError), id="required"),
- pytest.param(INPUT_DICT, "missing_required", None, True, "custom_org_key", None, None, pytest.raises(ValueError), id="custom org_key"),
- pytest.param(INPUT_DICT, "nested_test||nested_value", None, None, None, "||", 43, does_not_raise(), id="custom separator"),
- ],
-)
-def test_get_value(
- input_dict: dict[Any, Any],
- key: str,
- default: str | None,
- required: bool,
- org_key: str | None,
- separator: str | None,
- expected_result: str,
- expected_raise: Any,
-) -> None:
- """
- Test get_value
- """
- # pylint: disable=too-many-arguments
- kwargs = {"default": default, "required": required, "org_key": org_key, "separator": separator}
- kwargs = {k: v for k, v in kwargs.items() if v is not None}
- with expected_raise:
- assert get_value(input_dict, key, **kwargs) == expected_result # type: ignore
diff --git a/tests/units/tools/test_misc.py b/tests/units/tools/test_misc.py
deleted file mode 100644
index c453c21..0000000
--- a/tests/units/tools/test_misc.py
+++ /dev/null
@@ -1,38 +0,0 @@
-# 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.
-"""
-Tests for anta.tools.misc
-"""
-from __future__ import annotations
-
-import pytest
-
-from anta.tools.misc import exc_to_str, tb_to_str
-
-
-def my_raising_function(exception: Exception) -> None:
- """
- dummy function to raise Exception
- """
- raise exception
-
-
-@pytest.mark.parametrize("exception, expected_output", [(ValueError("test"), "ValueError (test)"), (ValueError(), "ValueError")])
-def test_exc_to_str(exception: Exception, expected_output: str) -> None:
- """
- Test exc_to_str
- """
- assert exc_to_str(exception) == expected_output
-
-
-def test_tb_to_str() -> None:
- """
- Test tb_to_str
- """
- try:
- my_raising_function(ValueError("test"))
- except ValueError as e:
- output = tb_to_str(e)
- assert "Traceback" in output
- assert 'my_raising_function(ValueError("test"))' in output
diff --git a/tests/units/tools/test_utils.py b/tests/units/tools/test_utils.py
deleted file mode 100644
index 448324f..0000000
--- a/tests/units/tools/test_utils.py
+++ /dev/null
@@ -1,57 +0,0 @@
-# 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.
-
-"""Tests for `anta.tools.utils`."""
-from __future__ import annotations
-
-from contextlib import nullcontext as does_not_raise
-from typing import Any
-
-import pytest
-
-from anta.tools.utils import get_failed_logs
-
-EXPECTED_OUTPUTS = [
- {"id": 1, "name": "Alice", "age": 30, "email": "alice@example.com"},
- {"id": 2, "name": "Bob", "age": 35, "email": "bob@example.com"},
- {"id": 3, "name": "Charlie", "age": 40, "email": "charlie@example.com"},
- {"id": 4, "name": "Jon", "age": 25, "email": "Jon@example.com"},
-]
-
-ACTUAL_OUTPUTS = [
- {"id": 1, "name": "Alice", "age": 30, "email": "alice@example.com"},
- {"id": 2, "name": "Bob", "age": 35, "email": "bob@example.com"},
- {"id": 3, "name": "Charlie", "age": 40, "email": "charlie@example.com"},
- {"id": 4, "name": "Rob", "age": 25, "email": "Jon@example.com"},
-]
-
-
-@pytest.mark.parametrize(
- "expected_output, actual_output, expected_result, expected_raise",
- [
- pytest.param(EXPECTED_OUTPUTS[0], ACTUAL_OUTPUTS[0], "", does_not_raise(), id="no difference"),
- pytest.param(
- EXPECTED_OUTPUTS[0],
- ACTUAL_OUTPUTS[1],
- "\nExpected `1` as the id, but found `2` instead.\nExpected `Alice` as the name, but found `Bob` instead.\n"
- "Expected `30` as the age, but found `35` instead.\nExpected `alice@example.com` as the email, but found `bob@example.com` instead.",
- does_not_raise(),
- id="different data",
- ),
- pytest.param(
- EXPECTED_OUTPUTS[0],
- {},
- "\nExpected `1` as the id, but it was not found in the actual output.\nExpected `Alice` as the name, but it was not found in the actual output.\n"
- "Expected `30` as the age, but it was not found in the actual output.\nExpected `alice@example.com` as the email, but it was not found in "
- "the actual output.",
- does_not_raise(),
- id="empty actual output",
- ),
- pytest.param(EXPECTED_OUTPUTS[3], ACTUAL_OUTPUTS[3], "\nExpected `Jon` as the name, but found `Rob` instead.", does_not_raise(), id="different name"),
- ],
-)
-def test_get_failed_logs(expected_output: dict[Any, Any], actual_output: dict[Any, Any], expected_result: str, expected_raise: Any) -> None:
- """Test get_failed_logs."""
- with expected_raise:
- assert get_failed_logs(expected_output, actual_output) == expected_result