Skip to content

Commit

Permalink
fix(anta.tests): AntaTest.Input subclasses using common input models …
Browse files Browse the repository at this point in the history
…should have validators for their required fields. (#1013)

* Added validators for required fields

* added unit tests for input validators

* Fix docstrings

---------

Co-authored-by: Carl Baillargeon <carl.baillargeon@arista.com>
  • Loading branch information
vitthalmagadum and carl-baillargeon authored Jan 20, 2025
1 parent ab57f89 commit e0545a8
Show file tree
Hide file tree
Showing 6 changed files with 139 additions and 11 deletions.
4 changes: 2 additions & 2 deletions anta/input_models/routing/bgp.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,8 +224,8 @@ class BgpRoute(BaseModel):
"""The IPv4 network address."""
vrf: str = "default"
"""Optional VRF for the BGP peer. Defaults to `default`."""
paths: list[BgpRoutePath] | None = None
"""A list of paths for the BGP route. Required field in the `VerifyBGPRouteOrigin` test."""
paths: list[BgpRoutePath]
"""A list of paths for the BGP route."""

def __str__(self) -> str:
"""Return a human-readable string representation of the BgpRoute for reporting.
Expand Down
6 changes: 3 additions & 3 deletions anta/input_models/routing/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ class IPv4Routes(BaseModel):

model_config = ConfigDict(extra="forbid")
prefix: IPv4Network
"""The IPV4 network to validate the route type."""
"""IPv4 prefix in CIDR notation."""
vrf: str = "default"
"""VRF context. Defaults to `default` VRF."""
route_type: IPv4RouteType
"""List of IPV4 Route type to validate the valid rout type."""
route_type: IPv4RouteType | None = None
"""Expected route type. Required field in the `VerifyIPv4RouteType` test."""

def __str__(self) -> str:
"""Return a human-readable string representation of the IPv4RouteType for reporting."""
Expand Down
27 changes: 25 additions & 2 deletions anta/tests/interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@

import re
from ipaddress import IPv4Interface
from typing import Any, ClassVar
from typing import Any, ClassVar, TypeVar

from pydantic import BaseModel, Field
from pydantic import BaseModel, Field, field_validator
from pydantic_extra_types.mac_address import MacAddress

from anta import GITHUB_SUGGESTION
Expand All @@ -23,6 +23,9 @@

BPS_GBPS_CONVERSIONS = 1000000000

# Using a TypeVar for the InterfaceState model since mypy thinks it's a ClassVar and not a valid type when used in field validators
T = TypeVar("T", bound=InterfaceState)


class VerifyInterfaceUtilization(AntaTest):
"""Verifies that the utilization of interfaces is below a certain threshold.
Expand Down Expand Up @@ -226,6 +229,16 @@ class Input(AntaTest.Input):
"""List of interfaces with their expected state."""
InterfaceState: ClassVar[type[InterfaceState]] = InterfaceState

@field_validator("interfaces")
@classmethod
def validate_interfaces(cls, interfaces: list[T]) -> list[T]:
"""Validate that 'status' field is provided in each interface."""
for interface in interfaces:
if interface.status is None:
msg = f"{interface} 'status' field missing in the input"
raise ValueError(msg)
return interfaces

@AntaTest.anta_test
def test(self) -> None:
"""Main test function for VerifyInterfacesStatus."""
Expand Down Expand Up @@ -891,6 +904,16 @@ class Input(AntaTest.Input):
"""List of interfaces with their expected state."""
InterfaceState: ClassVar[type[InterfaceState]] = InterfaceState

@field_validator("interfaces")
@classmethod
def validate_interfaces(cls, interfaces: list[T]) -> list[T]:
"""Validate that 'portchannel' field is provided in each interface."""
for interface in interfaces:
if interface.portchannel is None:
msg = f"{interface} 'portchannel' field missing in the input"
raise ValueError(msg)
return interfaces

@AntaTest.anta_test
def test(self) -> None:
"""Main test function for VerifyLACPInterfacesStatus."""
Expand Down
20 changes: 16 additions & 4 deletions anta/tests/routing/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from ipaddress import IPv4Address, IPv4Interface
from typing import TYPE_CHECKING, ClassVar, Literal

from pydantic import model_validator
from pydantic import field_validator, model_validator

from anta.custom_types import PositiveInteger
from anta.input_models.routing.generic import IPv4Routes
Expand Down Expand Up @@ -189,9 +189,10 @@ class VerifyIPv4RouteType(AntaTest):
"""Verifies the route-type of the IPv4 prefixes.
This test performs the following checks for each IPv4 route:
1. Verifies that the specified VRF is configured.
2. Verifies that the specified IPv4 route is exists in the configuration.
3. Verifies that the the specified IPv4 route is of the expected type.
1. Verifies that the specified VRF is configured.
2. Verifies that the specified IPv4 route is exists in the configuration.
3. Verifies that the the specified IPv4 route is of the expected type.
Expected Results
----------------
Expand Down Expand Up @@ -230,6 +231,17 @@ class Input(AntaTest.Input):
"""Input model for the VerifyIPv4RouteType test."""

routes_entries: list[IPv4Routes]
"""List of IPv4 route(s)."""

@field_validator("routes_entries")
@classmethod
def validate_routes_entries(cls, routes_entries: list[IPv4Routes]) -> list[IPv4Routes]:
"""Validate that 'route_type' field is provided in each BGP route entry."""
for entry in routes_entries:
if entry.route_type is None:
msg = f"{entry} 'route_type' field missing in the input"
raise ValueError(msg)
return routes_entries

@AntaTest.anta_test
def test(self) -> None:
Expand Down
41 changes: 41 additions & 0 deletions tests/units/input_models/routing/test_generic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Copyright (c) 2023-2025 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.input_models.routing.generic.py."""

from __future__ import annotations

from typing import TYPE_CHECKING

import pytest
from pydantic import ValidationError

from anta.tests.routing.generic import VerifyIPv4RouteType

if TYPE_CHECKING:
from anta.input_models.routing.generic import IPv4Routes


class TestVerifyIPv4RouteTypeInput:
"""Test anta.tests.routing.bgp.VerifyIPv4RouteType.Input."""

@pytest.mark.parametrize(
("routes_entries"),
[
pytest.param([{"prefix": "192.168.0.0/24", "vrf": "default", "route_type": "eBGP"}], id="valid"),
],
)
def test_valid(self, routes_entries: list[IPv4Routes]) -> None:
"""Test VerifyIPv4RouteType.Input valid inputs."""
VerifyIPv4RouteType.Input(routes_entries=routes_entries)

@pytest.mark.parametrize(
("routes_entries"),
[
pytest.param([{"prefix": "192.168.0.0/24", "vrf": "default"}], id="invalid"),
],
)
def test_invalid(self, routes_entries: list[IPv4Routes]) -> None:
"""Test VerifyIPv4RouteType.Input invalid inputs."""
with pytest.raises(ValidationError):
VerifyIPv4RouteType.Input(routes_entries=routes_entries)
52 changes: 52 additions & 0 deletions tests/units/input_models/test_interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@
from typing import TYPE_CHECKING

import pytest
from pydantic import ValidationError

from anta.input_models.interfaces import InterfaceState
from anta.tests.interfaces import VerifyInterfacesStatus, VerifyLACPInterfacesStatus

if TYPE_CHECKING:
from anta.custom_types import Interface, PortChannelInterface
Expand All @@ -31,3 +33,53 @@ class TestInterfaceState:
def test_valid__str__(self, name: Interface, portchannel: PortChannelInterface | None, expected: str) -> None:
"""Test InterfaceState __str__."""
assert str(InterfaceState(name=name, portchannel=portchannel)) == expected


class TestVerifyInterfacesStatusInput:
"""Test anta.tests.interfaces.VerifyInterfacesStatus.Input."""

@pytest.mark.parametrize(
("interfaces"),
[
pytest.param([{"name": "Ethernet1", "status": "up"}], id="valid"),
],
)
def test_valid(self, interfaces: list[InterfaceState]) -> None:
"""Test VerifyInterfacesStatus.Input valid inputs."""
VerifyInterfacesStatus.Input(interfaces=interfaces)

@pytest.mark.parametrize(
("interfaces"),
[
pytest.param([{"name": "Ethernet1"}], id="invalid"),
],
)
def test_invalid(self, interfaces: list[InterfaceState]) -> None:
"""Test VerifyInterfacesStatus.Input invalid inputs."""
with pytest.raises(ValidationError):
VerifyInterfacesStatus.Input(interfaces=interfaces)


class TestVerifyLACPInterfacesStatusInput:
"""Test anta.tests.interfaces.VerifyLACPInterfacesStatus.Input."""

@pytest.mark.parametrize(
("interfaces"),
[
pytest.param([{"name": "Ethernet1", "portchannel": "Port-Channel100"}], id="valid"),
],
)
def test_valid(self, interfaces: list[InterfaceState]) -> None:
"""Test VerifyLACPInterfacesStatus.Input valid inputs."""
VerifyLACPInterfacesStatus.Input(interfaces=interfaces)

@pytest.mark.parametrize(
("interfaces"),
[
pytest.param([{"name": "Ethernet1"}], id="invalid"),
],
)
def test_invalid(self, interfaces: list[InterfaceState]) -> None:
"""Test VerifyLACPInterfacesStatus.Input invalid inputs."""
with pytest.raises(ValidationError):
VerifyLACPInterfacesStatus.Input(interfaces=interfaces)

0 comments on commit e0545a8

Please sign in to comment.