Repository: aristanetworks/anta (python)

Issue:
VerifyUnifiedForwardingTableMode should be skipped on unsupported platforms
```bash
show platform trident forwarding-table partition has failed: Invalid input (at token 2: 'trident')
```

Pull request:
fix(anta.tests): VerifyUnifiedForwardingTableMode test is now skipped on unsupported platforms
Fixes #607 

# Checklist:

<!-- Delete not relevant items !-->

- [X] My code follows the style guidelines of this project
- [X] I have performed a self-review of my code
- [X] I have run pre-commit for code linting and typing (`pre-commit run`)
- [X] I have made corresponding changes to the documentation
- [X] I have added tests that prove my fix is effective or that my feature works
- [X] New and existing unit tests pass locally with my changes (`tox -e testenv`)


Patch:
diff --git a/anta/constants.py b/anta/constants.py
index ae131dd1..502dfe4a 100644
--- a/anta/constants.py
+++ b/anta/constants.py
@@ -45,3 +45,11 @@ KNOWN_EOS_ERRORS = [
     r"No source interface .*",
 ]
 """List of known EOS errors that should set a test status to 'failure' with the error message."""
+
+UNSUPPORTED_PLATFORM_ERRORS = [
+    "not supported on this hardware platform",
+    "Invalid input (at token 2: 'trident')",
+]
+"""Error messages indicating platform or hardware unsupported commands.
+Will set the test status to 'skipped'. Includes both general hardware
+platform errors and specific ASIC family limitations."""
diff --git a/anta/input_models/routing/bgp.py b/anta/input_models/routing/bgp.py
index 72819238..a34227a1 100644
--- a/anta/input_models/routing/bgp.py
+++ b/anta/input_models/routing/bgp.py
@@ -6,7 +6,7 @@
 from __future__ import annotations
 
 from ipaddress import IPv4Address, IPv4Network, IPv6Address
-from typing import TYPE_CHECKING, Any
+from typing import TYPE_CHECKING, Any, Literal
 from warnings import warn
 
 from pydantic import BaseModel, ConfigDict, Field, PositiveInt, model_validator
@@ -211,3 +211,46 @@ class VxlanEndpoint(BaseModel):
     def __str__(self) -> str:
         """Return a human-readable string representation of the VxlanEndpoint for reporting."""
         return f"Address: {self.address} VNI: {self.vni}"
+
+
+class BgpRoute(BaseModel):
+    """Model representing BGP routes.
+
+    Only IPv4 prefixes are supported for now.
+    """
+
+    model_config = ConfigDict(extra="forbid")
+    prefix: IPv4Network
+    """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."""
+
+    def __str__(self) -> str:
+        """Return a human-readable string representation of the BgpRoute for reporting.
+
+        Examples
+        --------
+        - Prefix: 192.168.66.100/24 VRF: default
+        """
+        return f"Prefix: {self.prefix} VRF: {self.vrf}"
+
+
+class BgpRoutePath(BaseModel):
+    """Model representing a BGP route path."""
+
+    model_config = ConfigDict(extra="forbid")
+    nexthop: IPv4Address
+    """The next-hop IPv4 address for the path."""
+    origin: Literal["Igp", "Egp", "Incomplete"]
+    """The BGP origin attribute of the route."""
+
+    def __str__(self) -> str:
+        """Return a human-readable string representation of the RoutePath for reporting.
+
+        Examples
+        --------
+        - Next-hop: 192.168.66.101 Origin: Igp
+        """
+        return f"Next-hop: {self.nexthop} Origin: {self.origin}"
diff --git a/anta/models.py b/anta/models.py
index 71f33ebd..ce2482f0 100644
--- a/anta/models.py
+++ b/anta/models.py
@@ -15,8 +15,7 @@ from typing import TYPE_CHECKING, Any, Callable, ClassVar, Literal, TypeVar
 
 from pydantic import BaseModel, ConfigDict, ValidationError, create_model
 
-from anta import GITHUB_SUGGESTION
-from anta.constants import KNOWN_EOS_ERRORS
+from anta.constants import KNOWN_EOS_ERRORS, UNSUPPORTED_PLATFORM_ERRORS
 from anta.custom_types import REGEXP_EOS_BLACKLIST_CMDS, Revision
 from anta.logger import anta_log_exception, exc_to_str
 from anta.result_manager.models import AntaTestStatus, TestResult
@@ -258,7 +257,8 @@ class AntaCommand(BaseModel):
             msg = f"Command '{self.command}' has not been collected and has not returned an error. Call AntaDevice.collect()."
 
             raise RuntimeError(msg)
-        return all("not supported on this hardware platform" not in e for e in self.errors)
+
+        return not any(any(error in e for error in UNSUPPORTED_PLATFORM_ERRORS) for e in self.errors)
 
     @property
     def returned_known_eos_error(self) -> bool:
@@ -683,8 +683,6 @@ class AntaTest(ABC):
         cmds = self.failed_commands
         unsupported_commands = [f"'{c.command}' is not supported on {self.device.hw_model}" for c in cmds if not c.supported]
         if unsupported_commands:
-            msg = f"Test {self.name} has been skipped because it is not supported on {self.device.hw_model}: {GITHUB_SUGGESTION}"
-            self.logger.warning(msg)
             self.result.is_skipped("\n".join(unsupported_commands))
             return
         returned_known_eos_error = [f"'{c.command}' failed on {self.device.name}: {', '.join(c.errors)}" for c in cmds if c.returned_known_eos_error]


Test patch:
diff --git a/anta/tests/profiles.py b/anta/tests/profiles.py
index f9c3c262..9f78a170 100644
--- a/anta/tests/profiles.py
+++ b/anta/tests/profiles.py
@@ -51,7 +51,7 @@ class VerifyUnifiedForwardingTableMode(AntaTest):
         if command_output["uftMode"] == str(self.inputs.mode):
             self.result.is_success()
         else:
-            self.result.is_failure(f"Device is not running correct UFT mode (expected: {self.inputs.mode} / running: {command_output['uftMode']})")
+            self.result.is_failure(f"Not running the correct UFT mode - Expected: {self.inputs.mode}, Actual: {command_output['uftMode']}")
 
 
 class VerifyTcamProfile(AntaTest):
diff --git a/anta/tests/routing/bgp.py b/anta/tests/routing/bgp.py
index c8865026..1b85b589 100644
--- a/anta/tests/routing/bgp.py
+++ b/anta/tests/routing/bgp.py
@@ -11,7 +11,7 @@ from typing import ClassVar, TypeVar
 
 from pydantic import field_validator
 
-from anta.input_models.routing.bgp import BgpAddressFamily, BgpAfi, BgpNeighbor, BgpPeer, VxlanEndpoint
+from anta.input_models.routing.bgp import BgpAddressFamily, BgpAfi, BgpNeighbor, BgpPeer, BgpRoute, VxlanEndpoint
 from anta.models import AntaCommand, AntaTemplate, AntaTest
 from anta.tools import format_data, get_item, get_value
 
@@ -1619,3 +1619,69 @@ class VerifyBGPNlriAcceptance(AntaTest):
 
                 if (received := capability_status.get("nlrisReceived")) != (accepted := capability_status.get("nlrisAccepted")):
                     self.result.is_failure(f"{peer} AFI/SAFI: {capability} - some NLRI were filtered or rejected - Accepted: {accepted} Received: {received}")
+
+
+class VerifyBGPRoutePaths(AntaTest):
+    """Verifies BGP IPv4 route paths.
+
+    This test performs the following checks for each specified BGP route entry:
+      1. Verifies the specified BGP route exists in the routing table.
+      2. For each expected paths:
+          - Verifies a path with matching next-hop exists.
+          - Verifies the path's origin attribute matches the expected value.
+
+    Expected Results
+    ----------------
+    * Success: The test will pass if all specified routes exist with paths matching the expected next-hops and origin attributes.
+    * Failure: The test will fail if:
+        - A specified BGP route is not found.
+        - A path with specified next-hop is not found.
+        - A path's origin attribute doesn't match the expected value.
+
+    Examples
+    --------
+    ```yaml
+    anta.tests.routing:
+      bgp:
+        - VerifyBGPRoutePaths:
+            route_entries:
+                - prefix: 10.100.0.128/31
+                  vrf: default
+                  paths:
+                    - nexthop: 10.100.0.10
+                      origin: Igp
+                    - nexthop: 10.100.4.5
+                      origin: Incomplete
+    ```
+    """
+
+    categories: ClassVar[list[str]] = ["bgp"]
+    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show ip bgp vrf all", revision=3)]
+
+    class Input(AntaTest.Input):
+        """Input model for the VerifyBGPRoutePaths test."""
+
+        route_entries: list[BgpRoute]
+        """List of BGP IPv4 route(s)."""
+
+    @AntaTest.anta_test
+    def test(self) -> None:
+        """Main test function for VerifyBGPRoutePaths."""
+        self.result.is_success()
+
+        for route in self.inputs.route_entries:
+            # Verify if the prefix exists in BGP table
+            if not (bgp_routes := get_value(self.instance_commands[0].json_output, f"vrfs..{route.vrf}..bgpRouteEntries..{route.prefix}", separator="..")):
+                self.result.is_failure(f"{route} - prefix not found")
+                continue
+
+            # Iterating over each path.
+            for path in route.paths:
+                nexthop = str(path.nexthop)
+                origin = path.origin
+                if not (route_path := get_item(bgp_routes["bgpRoutePaths"], "nextHop", nexthop)):
+                    self.result.is_failure(f"{route} {path} - path not found")
+                    continue
+
+                if (actual_origin := get_value(route_path, "routeType.origin")) != origin:
+                    self.result.is_failure(f"{route} {path} - Origin mismatch - Actual: {actual_origin}")
diff --git a/examples/tests.yaml b/examples/tests.yaml
index 87fb1455..9aaf15e6 100644
--- a/examples/tests.yaml
+++ b/examples/tests.yaml
@@ -506,6 +506,16 @@ anta.tests.routing.bgp:
   - VerifyBGPPeersHealthRibd:
       # Verifies the health of all the BGP IPv4 peer(s).
       check_tcp_queues: True
+  - VerifyBGPRoutePaths:
+      # Verifies BGP IPv4 route paths.
+      route_entries:
+          - prefix: 10.100.0.128/31
+            vrf: default
+            paths:
+              - nexthop: 10.100.0.10
+                origin: Igp
+              - nexthop: 10.100.4.5
+                origin: Incomplete
   - VerifyBGPSpecificPeers:
       # Verifies the health of specific BGP peer(s) for given address families.
       address_families:
diff --git a/tests/units/anta_tests/routing/test_bgp.py b/tests/units/anta_tests/routing/test_bgp.py
index fa95b755..21758ea7 100644
--- a/tests/units/anta_tests/routing/test_bgp.py
+++ b/tests/units/anta_tests/routing/test_bgp.py
@@ -29,6 +29,7 @@ from anta.tests.routing.bgp import (
     VerifyBGPPeersHealthRibd,
     VerifyBGPPeerUpdateErrors,
     VerifyBgpRouteMaps,
+    VerifyBGPRoutePaths,
     VerifyBGPSpecificPeers,
     VerifyBGPTimers,
     VerifyEVPNType2Route,
@@ -4875,4 +4876,228 @@ DATA: list[dict[str, Any]] = [
             ],
         },
     },
+    {
+        "name": "success",
+        "test": VerifyBGPRoutePaths,
+        "eos_data": [
+            {
+                "vrfs": {
+                    "default": {
+                        "bgpRouteEntries": {
+                            "10.100.0.128/31": {
+                                "bgpRoutePaths": [
+                                    {
+                                        "nextHop": "10.100.0.10",
+                                        "routeType": {
+                                            "origin": "Igp",
+                                        },
+                                    },
+                                    {
+                                        "nextHop": "10.100.4.5",
+                                        "routeType": {
+                                            "origin": "Incomplete",
+                                        },
+                                    },
+                                ],
+                            }
+                        }
+                    },
+                    "MGMT": {
+                        "bgpRouteEntries": {
+                            "10.100.0.130/31": {
+                                "bgpRoutePaths": [
+                                    {
+                                        "nextHop": "10.100.0.8",
+                                        "routeType": {
+                                            "origin": "Igp",
+                                        },
+                                    },
+                                    {
+                                        "nextHop": "10.100.0.10",
+                                        "routeType": {
+                                            "origin": "Igp",
+                                        },
+                                    },
+                                ],
+                            }
+                        }
+                    },
+                }
+            },
+        ],
+        "inputs": {
+            "route_entries": [
+                {
+                    "prefix": "10.100.0.128/31",
+                    "vrf": "default",
+                    "paths": [{"nexthop": "10.100.0.10", "origin": "Igp"}, {"nexthop": "10.100.4.5", "origin": "Incomplete"}],
+                },
+                {
+                    "prefix": "10.100.0.130/31",
+                    "vrf": "MGMT",
+                    "paths": [{"nexthop": "10.100.0.8", "origin": "Igp"}, {"nexthop": "10.100.0.10", "origin": "Igp"}],
+                },
+            ]
+        },
+        "expected": {"result": "success"},
+    },
+    {
+        "name": "failure-origin-not-correct",
+        "test": VerifyBGPRoutePaths,
+        "eos_data": [
+            {
+                "vrfs": {
+                    "default": {
+                        "bgpRouteEntries": {
+                            "10.100.0.128/31": {
+                                "bgpRoutePaths": [
+                                    {
+                                        "nextHop": "10.100.0.10",
+                                        "routeType": {
+                                            "origin": "Igp",
+                                        },
+                                    },
+                                    {
+                                        "nextHop": "10.100.4.5",
+                                        "routeType": {
+                                            "origin": "Incomplete",
+                                        },
+                                    },
+                                ],
+                            }
+                        }
+                    },
+                    "MGMT": {
+                        "bgpRouteEntries": {
+                            "10.100.0.130/31": {
+                                "bgpRoutePaths": [
+                                    {
+                                        "nextHop": "10.100.0.8",
+                                        "routeType": {
+                                            "origin": "Igp",
+                                        },
+                                    },
+                                    {
+                                        "nextHop": "10.100.0.10",
+                                        "routeType": {
+                                            "origin": "Igp",
+                                        },
+                                    },
+                                ],
+                            }
+                        }
+                    },
+                }
+            },
+        ],
+        "inputs": {
+            "route_entries": [
+                {
+                    "prefix": "10.100.0.128/31",
+                    "vrf": "default",
+                    "paths": [{"nexthop": "10.100.0.10", "origin": "Incomplete"}, {"nexthop": "10.100.4.5", "origin": "Igp"}],
+                },
+                {
+                    "prefix": "10.100.0.130/31",
+                    "vrf": "MGMT",
+                    "paths": [{"nexthop": "10.100.0.8", "origin": "Incomplete"}, {"nexthop": "10.100.0.10", "origin": "Incomplete"}],
+                },
+            ]
+        },
+        "expected": {
+            "result": "failure",
+            "messages": [
+                "Prefix: 10.100.0.128/31 VRF: default Next-hop: 10.100.0.10 Origin: Incomplete - Origin mismatch - Actual: Igp",
+                "Prefix: 10.100.0.128/31 VRF: default Next-hop: 10.100.4.5 Origin: Igp - Origin mismatch - Actual: Incomplete",
+                "Prefix: 10.100.0.130/31 VRF: MGMT Next-hop: 10.100.0.8 Origin: Incomplete - Origin mismatch - Actual: Igp",
+                "Prefix: 10.100.0.130/31 VRF: MGMT Next-hop: 10.100.0.10 Origin: Incomplete - Origin mismatch - Actual: Igp",
+            ],
+        },
+    },
+    {
+        "name": "failure-path-not-found",
+        "test": VerifyBGPRoutePaths,
+        "eos_data": [
+            {
+                "vrfs": {
+                    "default": {
+                        "bgpRouteEntries": {
+                            "10.100.0.128/31": {
+                                "bgpRoutePaths": [
+                                    {
+                                        "nextHop": "10.100.0.15",
+                                        "routeType": {
+                                            "origin": "Igp",
+                                        },
+                                    },
+                                ],
+                            }
+                        }
+                    },
+                    "MGMT": {
+                        "bgpRouteEntries": {
+                            "10.100.0.130/31": {
+                                "bgpRoutePaths": [
+                                    {
+                                        "nextHop": "10.100.0.15",
+                                        "routeType": {
+                                            "origin": "Igp",
+                                        },
+                                    },
+                                ],
+                            }
+                        }
+                    },
+                }
+            },
+        ],
+        "inputs": {
+            "route_entries": [
+                {
+                    "prefix": "10.100.0.128/31",
+                    "vrf": "default",
+                    "paths": [{"nexthop": "10.100.0.10", "origin": "Incomplete"}, {"nexthop": "10.100.4.5", "origin": "Igp"}],
+                },
+                {
+                    "prefix": "10.100.0.130/31",
+                    "vrf": "MGMT",
+                    "paths": [{"nexthop": "10.100.0.8", "origin": "Incomplete"}, {"nexthop": "10.100.0.10", "origin": "Incomplete"}],
+                },
+            ]
+        },
+        "expected": {
+            "result": "failure",
+            "messages": [
+                "Prefix: 10.100.0.128/31 VRF: default Next-hop: 10.100.0.10 Origin: Incomplete - path not found",
+                "Prefix: 10.100.0.128/31 VRF: default Next-hop: 10.100.4.5 Origin: Igp - path not found",
+                "Prefix: 10.100.0.130/31 VRF: MGMT Next-hop: 10.100.0.8 Origin: Incomplete - path not found",
+                "Prefix: 10.100.0.130/31 VRF: MGMT Next-hop: 10.100.0.10 Origin: Incomplete - path not found",
+            ],
+        },
+    },
+    {
+        "name": "failure-prefix-not-found",
+        "test": VerifyBGPRoutePaths,
+        "eos_data": [
+            {"vrfs": {"default": {"bgpRouteEntries": {}}, "MGMT": {"bgpRouteEntries": {}}}},
+        ],
+        "inputs": {
+            "route_entries": [
+                {
+                    "prefix": "10.100.0.128/31",
+                    "vrf": "default",
+                    "paths": [{"nexthop": "10.100.0.10", "origin": "Incomplete"}, {"nexthop": "10.100.4.5", "origin": "Igp"}],
+                },
+                {
+                    "prefix": "10.100.0.130/31",
+                    "vrf": "MGMT",
+                    "paths": [{"nexthop": "10.100.0.8", "origin": "Incomplete"}, {"nexthop": "10.100.0.10", "origin": "Incomplete"}],
+                },
+            ]
+        },
+        "expected": {
+            "result": "failure",
+            "messages": ["Prefix: 10.100.0.128/31 VRF: default - prefix not found", "Prefix: 10.100.0.130/31 VRF: MGMT - prefix not found"],
+        },
+    },
 ]
diff --git a/tests/units/anta_tests/test_profiles.py b/tests/units/anta_tests/test_profiles.py
index 81ef4f9f..63c75c5d 100644
--- a/tests/units/anta_tests/test_profiles.py
+++ b/tests/units/anta_tests/test_profiles.py
@@ -23,7 +23,7 @@ DATA: list[dict[str, Any]] = [
         "test": VerifyUnifiedForwardingTableMode,
         "eos_data": [{"uftMode": "2", "urpfEnabled": False, "chipModel": "bcm56870", "l2TableSize": 163840, "l3TableSize": 147456, "lpmTableSize": 32768}],
         "inputs": {"mode": 3},
-        "expected": {"result": "failure", "messages": ["Device is not running correct UFT mode (expected: 3 / running: 2)"]},
+        "expected": {"result": "failure", "messages": ["Not running the correct UFT mode - Expected: 3, Actual: 2"]},
     },
     {
         "name": "success",