diff options
Diffstat (limited to 'anta/reporter/__init__.py')
-rw-r--r-- | anta/reporter/__init__.py | 195 |
1 files changed, 100 insertions, 95 deletions
diff --git a/anta/reporter/__init__.py b/anta/reporter/__init__.py index dda9d9c..3e068f5 100644 --- a/anta/reporter/__init__.py +++ b/anta/reporter/__init__.py @@ -1,23 +1,25 @@ # 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. -""" -Report management for ANTA. -""" +"""Report management for ANTA.""" + # pylint: disable = too-few-public-methods from __future__ import annotations import logging -import os.path -import pathlib -from typing import Any, Optional +from typing import TYPE_CHECKING, Any from jinja2 import Template from rich.table import Table from anta import RICH_COLOR_PALETTE, RICH_COLOR_THEME -from anta.custom_types import TestStatus -from anta.result_manager import ResultManager + +if TYPE_CHECKING: + import pathlib + + from anta.custom_types import TestStatus + from anta.result_manager import ResultManager + from anta.result_manager.models import TestResult logger = logging.getLogger(__name__) @@ -25,33 +27,37 @@ logger = logging.getLogger(__name__) class ReportTable: """TableReport Generate a Table based on TestResult.""" - def _split_list_to_txt_list(self, usr_list: list[str], delimiter: Optional[str] = None) -> str: - """ - Split list to multi-lines string + def _split_list_to_txt_list(self, usr_list: list[str], delimiter: str | None = None) -> str: + """Split list to multi-lines string. Args: + ---- usr_list (list[str]): List of string to concatenate delimiter (str, optional): A delimiter to use to start string. Defaults to None. - Returns: + Returns + ------- str: Multi-lines string + """ if delimiter is not None: return "\n".join(f"{delimiter} {line}" for line in usr_list) return "\n".join(f"{line}" for line in usr_list) def _build_headers(self, headers: list[str], table: Table) -> Table: - """ - Create headers for a table. + """Create headers for a table. First key is considered as header and is colored using RICH_COLOR_PALETTE.HEADER Args: - headers (list[str]): List of headers - table (Table): A rich Table instance + ---- + headers: List of headers. + table: A rich Table instance. + + Returns + ------- + A rich `Table` instance with headers. - Returns: - Table: A rich Table instance with headers """ for idx, header in enumerate(headers): if idx == 0: @@ -64,72 +70,69 @@ class ReportTable: return table def _color_result(self, status: TestStatus) -> str: - """ - Return a colored string based on the status value. + """Return a colored string based on the status value. Args: - status (TestStatus): status value to color + ---- + status (TestStatus): status value to color. - Returns: + Returns + ------- str: the colored string + """ color = RICH_COLOR_THEME.get(status, "") return f"[{color}]{status}" if color != "" else str(status) - def report_all( - self, - result_manager: ResultManager, - host: Optional[str] = None, - testcase: Optional[str] = None, - title: str = "All tests results", - ) -> Table: - """ - Create a table report with all tests for one or all devices. + def report_all(self, manager: ResultManager, title: str = "All tests results") -> Table: + """Create a table report with all tests for one or all devices. Create table with full output: Host / Test / Status / Message Args: - result_manager (ResultManager): A manager with a list of tests. - host (str, optional): IP Address of a host to search for. Defaults to None. - testcase (str, optional): A test name to search for. Defaults to None. - title (str, optional): Title for the report. Defaults to 'All tests results'. + ---- + manager: A ResultManager instance. + title: Title for the report. Defaults to 'All tests results'. + + Returns + ------- + A fully populated rich `Table` - Returns: - Table: A fully populated rich Table """ table = Table(title=title, show_lines=True) headers = ["Device", "Test Name", "Test Status", "Message(s)", "Test description", "Test category"] table = self._build_headers(headers=headers, table=table) - for result in result_manager.get_results(): - # pylint: disable=R0916 - if (host is None and testcase is None) or (host is not None and str(result.name) == host) or (testcase is not None and testcase == str(result.test)): - state = self._color_result(result.result) - message = self._split_list_to_txt_list(result.messages) if len(result.messages) > 0 else "" - categories = ", ".join(result.categories) - table.add_row(str(result.name), result.test, state, message, result.description, categories) + def add_line(result: TestResult) -> None: + state = self._color_result(result.result) + message = self._split_list_to_txt_list(result.messages) if len(result.messages) > 0 else "" + categories = ", ".join(result.categories) + table.add_row(str(result.name), result.test, state, message, result.description, categories) + + for result in manager.results: + add_line(result) return table def report_summary_tests( self, - result_manager: ResultManager, - testcase: Optional[str] = None, - title: str = "Summary per test case", + manager: ResultManager, + tests: list[str] | None = None, + title: str = "Summary per test", ) -> Table: - """ - Create a table report with result agregated per test. + """Create a table report with result aggregated per test. - Create table with full output: Test / Number of success / Number of failure / Number of error / List of nodes in error or failure + Create table with full output: Test | Number of success | Number of failure | Number of error | List of nodes in error or failure Args: - result_manager (ResultManager): A manager with a list of tests. - testcase (str, optional): A test name to search for. Defaults to None. - title (str, optional): Title for the report. Defaults to 'All tests results'. - - Returns: - Table: A fully populated rich Table + ---- + manager: A ResultManager instance. + tests: List of test names to include. None to select all tests. + title: Title of the report. + + Returns + ------- + A fully populated rich `Table`. """ - # sourcery skip: class-extract-method table = Table(title=title, show_lines=True) headers = [ "Test Case", @@ -140,16 +143,16 @@ class ReportTable: "List of failed or error nodes", ] table = self._build_headers(headers=headers, table=table) - for testcase_read in result_manager.get_testcases(): - if testcase is None or str(testcase_read) == testcase: - results = result_manager.get_result_by_test(testcase_read) + for test in manager.get_tests(): + if tests is None or test in tests: + results = manager.filter_by_tests({test}).results nb_failure = len([result for result in results if result.result == "failure"]) nb_error = len([result for result in results if result.result == "error"]) - list_failure = [str(result.name) for result in results if result.result in ["failure", "error"]] + list_failure = [result.name for result in results if result.result in ["failure", "error"]] nb_success = len([result for result in results if result.result == "success"]) nb_skipped = len([result for result in results if result.result == "skipped"]) table.add_row( - testcase_read, + test, str(nb_success), str(nb_skipped), str(nb_failure), @@ -158,24 +161,25 @@ class ReportTable: ) return table - def report_summary_hosts( + def report_summary_devices( self, - result_manager: ResultManager, - host: Optional[str] = None, - title: str = "Summary per host", + manager: ResultManager, + devices: list[str] | None = None, + title: str = "Summary per device", ) -> Table: - """ - Create a table report with result agregated per host. + """Create a table report with result aggregated per device. - Create table with full output: Host / Number of success / Number of failure / Number of error / List of nodes in error or failure + Create table with full output: Host | Number of success | Number of failure | Number of error | List of nodes in error or failure Args: - result_manager (ResultManager): A manager with a list of tests. - host (str, optional): IP Address of a host to search for. Defaults to None. - title (str, optional): Title for the report. Defaults to 'All tests results'. - - Returns: - Table: A fully populated rich Table + ---- + manager: A ResultManager instance. + devices: List of device names to include. None to select all devices. + title: Title of the report. + + Returns + ------- + A fully populated rich `Table`. """ table = Table(title=title, show_lines=True) headers = [ @@ -187,18 +191,16 @@ class ReportTable: "List of failed or error test cases", ] table = self._build_headers(headers=headers, table=table) - for host_read in result_manager.get_hosts(): - if host is None or str(host_read) == host: - results = result_manager.get_result_by_host(host_read) - logger.debug("data to use for computation") - logger.debug(f"{host}: {results}") + for device in manager.get_devices(): + if devices is None or device in devices: + results = manager.filter_by_devices({device}).results nb_failure = len([result for result in results if result.result == "failure"]) nb_error = len([result for result in results if result.result == "error"]) - list_failure = [str(result.test) for result in results if result.result in ["failure", "error"]] + list_failure = [result.test for result in results if result.result in ["failure", "error"]] nb_success = len([result for result in results if result.result == "success"]) nb_skipped = len([result for result in results if result.result == "skipped"]) table.add_row( - str(host_read), + device, str(nb_success), str(nb_skipped), str(nb_failure), @@ -212,20 +214,20 @@ class ReportJinja: """Report builder based on a Jinja2 template.""" def __init__(self, template_path: pathlib.Path) -> None: - if os.path.isfile(template_path): + """Create a ReportJinja instance.""" + if template_path.is_file(): self.tempalte_path = template_path else: - raise FileNotFoundError(f"template file is not found: {template_path}") + msg = f"template file is not found: {template_path}" + raise FileNotFoundError(msg) - def render(self, data: list[dict[str, Any]], trim_blocks: bool = True, lstrip_blocks: bool = True) -> str: - """ - Build a report based on a Jinja2 template + def render(self, data: list[dict[str, Any]], *, trim_blocks: bool = True, lstrip_blocks: bool = True) -> str: + """Build a report based on a Jinja2 template. Report is built based on a J2 template provided by user. Data structure sent to template is: - >>> data = ResultManager.get_json_results() - >>> print(data) + >>> print(ResultManager.json) [ { name: ..., @@ -238,14 +240,17 @@ class ReportJinja: ] Args: - data (list[dict[str, Any]]): List of results from ResultManager.get_results - trim_blocks (bool, optional): enable trim_blocks for J2 rendering. Defaults to True. - lstrip_blocks (bool, optional): enable lstrip_blocks for J2 rendering. Defaults to True. + ---- + data: List of results from ResultManager.results + trim_blocks: enable trim_blocks for J2 rendering. + lstrip_blocks: enable lstrip_blocks for J2 rendering. + + Returns + ------- + Rendered template - Returns: - str: rendered template """ - with open(self.tempalte_path, encoding="utf-8") as file_: + with self.tempalte_path.open(encoding="utf-8") as file_: template = Template(file_.read(), trim_blocks=trim_blocks, lstrip_blocks=lstrip_blocks) return template.render({"data": data}) |