Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(anta): Add support of CSV file export #672

Merged
merged 43 commits into from
Jul 17, 2024
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
c272163
feat(anta): Add support of CSV file export
titom73 May 13, 2024
daa60d2
fix(anta.cli): Move --save-to-csv at NRFU level
titom73 May 16, 2024
9cdf026
fix(anta.cli): rename csv option to --csv-output
titom73 May 16, 2024
12e7b67
Merge branch 'main' into feat/csv-export
titom73 Jun 7, 2024
b4bf68e
Merge branch 'main' into feat/csv-export
titom73 Jul 1, 2024
66c812e
Merge branch 'main' into feat/csv-export
titom73 Jul 5, 2024
c8266e9
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 5, 2024
4e16158
fix(anta.reporter): Define constants to manage table headers - sonarc…
titom73 Jul 5, 2024
13a6b48
fix(anta.reporter): Define constants to manage table headers - sonarc…
titom73 Jul 5, 2024
dd4d0ab
Merge branch 'main' into feat/csv-export
titom73 Jul 5, 2024
09b6577
refactor: Move to a click command for csv
titom73 Jul 5, 2024
bf501e2
refactor: ReportCsv to dedicated module
titom73 Jul 5, 2024
030c502
Merge branch 'main' into feat/csv-export
titom73 Jul 5, 2024
29afb2f
Merge branch 'main' into feat/csv-export
titom73 Jul 5, 2024
f010737
refactor: Code review
titom73 Jul 5, 2024
3365125
Merge branch 'main' into feat/csv-export
gmuloc Jul 5, 2024
5dd6f96
fix: Implement unit test for anta nrfu csv
titom73 Jul 6, 2024
f9c0c8a
fix: Implement unit test for anta nrfu csv
titom73 Jul 6, 2024
2cb770a
fix: Implement unit test for anta nrfu csv
titom73 Jul 7, 2024
c166cdb
doc: Add anta nrfu csv initial documentation
titom73 Jul 8, 2024
fd0bdbb
Merge branch 'main' into feat/csv-export
titom73 Jul 8, 2024
734cd7c
doc: Add anta nrfu csv documentation
titom73 Jul 8, 2024
99bd02b
Merge branch 'main' into feat/csv-export
gmuloc Jul 9, 2024
c8b5dde
Update docs/cli/nrfu.md
titom73 Jul 10, 2024
25639c3
Update docs/snippets/anta_nrfu_help.txt
titom73 Jul 10, 2024
434a975
Update docs/cli/nrfu.md
titom73 Jul 10, 2024
c35d721
Update tests/units/reporter/test_csv.py
titom73 Jul 10, 2024
ef2bab7
Update tests/units/cli/nrfu/test_commands.py
titom73 Jul 10, 2024
e9be744
Update anta/reporter/csv_reporter.py
titom73 Jul 10, 2024
ccbce5f
Update anta/reporter/csv_reporter.py
titom73 Jul 10, 2024
c6c3a50
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 10, 2024
20d63c7
ci: Cleanup pytest
titom73 Jul 10, 2024
8173f2a
fix: Update cli help message
titom73 Jul 10, 2024
469496f
fix: Increase coverage: multiline
titom73 Jul 10, 2024
c572d85
fix: Update code
titom73 Jul 11, 2024
0a8f952
fix: Update code
titom73 Jul 11, 2024
1ad77d7
fix: Remove useless comment in CLI help message
titom73 Jul 12, 2024
a8626b6
update as per code review
titom73 Jul 16, 2024
e4a0197
fix: Capture error when opening CSV file
titom73 Jul 16, 2024
61a5629
doc: Fix doc
gmuloc Jul 16, 2024
00beea2
Refactor: Add test for OSError when writing csv report
gmuloc Jul 17, 2024
d92d829
Apply suggestions from code review
gmuloc Jul 17, 2024
3baf7ae
Test: Avoid creating test.csv in repo
gmuloc Jul 17, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions anta/cli/nrfu/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ def nrfu(


nrfu.add_command(commands.table)
nrfu.add_command(commands.csv)
nrfu.add_command(commands.json)
nrfu.add_command(commands.text)
nrfu.add_command(commands.tpl_report)
29 changes: 24 additions & 5 deletions anta/cli/nrfu/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

from anta.cli.utils import exit_with_code

from .utils import print_jinja, print_json, print_table, print_text, run_tests
from .utils import print_jinja, print_json, print_table, print_text, run_tests, save_to_csv

logger = logging.getLogger(__name__)

Expand All @@ -27,10 +27,7 @@
help="Group result by test or device.",
required=False,
)
def table(
ctx: click.Context,
group_by: Literal["device", "test"] | None,
) -> None:
def table(ctx: click.Context, group_by: Literal["device", "test"] | None) -> None:
"""ANTA command to check network states with table result."""
run_tests(ctx)
print_table(ctx, group_by=group_by)
Expand Down Expand Up @@ -63,6 +60,28 @@ def text(ctx: click.Context) -> None:
exit_with_code(ctx)


@click.command()
@click.pass_context
@click.option(
"--csv-output",
carl-baillargeon marked this conversation as resolved.
Show resolved Hide resolved
type=click.Path(
file_okay=True,
dir_okay=False,
exists=False,
writable=True,
path_type=pathlib.Path,
),
show_envvar=True,
required=False,
help="Path to save report as a CSV file. It only saves test results and not the output from --group-by option",
titom73 marked this conversation as resolved.
Show resolved Hide resolved
)
def csv(ctx: click.Context, csv_output: pathlib.Path) -> None:
"""ANTA command to check network states with CSV result."""
run_tests(ctx)
save_to_csv(ctx, csv_file=csv_output)
exit_with_code(ctx)


@click.command()
@click.pass_context
@click.option(
Expand Down
10 changes: 10 additions & 0 deletions anta/cli/nrfu/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@
from typing import TYPE_CHECKING, Literal

import rich
from rich.emoji import Emoji
gmuloc marked this conversation as resolved.
Show resolved Hide resolved
from rich.panel import Panel
from rich.progress import BarColumn, MofNCompleteColumn, Progress, SpinnerColumn, TextColumn, TimeElapsedColumn, TimeRemainingColumn

from anta.cli.console import console
from anta.models import AntaTest
from anta.reporter import ReportJinja, ReportTable
from anta.reporter.csv_reporter import ReportCsv
from anta.runner import main

if TYPE_CHECKING:
Expand Down Expand Up @@ -122,6 +124,14 @@ def print_jinja(results: ResultManager, template: pathlib.Path, output: pathlib.
file.write(report)


def save_to_csv(ctx: click.Context, csv_file: pathlib.Path | None = None) -> None:
"""Save results to a CSV file."""
if csv_file is not None:
ReportCsv().csv_report(results=_get_result_manager(ctx), csv_filename=csv_file)
carl-baillargeon marked this conversation as resolved.
Show resolved Hide resolved
checkmark = Emoji("white_check_mark")
console.print(f"CSV report saved to {csv_file} {checkmark}", style="cyan")
gmuloc marked this conversation as resolved.
Show resolved Hide resolved


# Adding our own ANTA spinner - overriding rich SPINNERS for our own
# so ignore warning for redefinition
rich.spinner.SPINNERS = { # type: ignore[attr-defined]
Expand Down
114 changes: 97 additions & 17 deletions anta/reporter/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,18 @@
# pylint: disable = too-few-public-methods
from __future__ import annotations

import csv
import logging
import pathlib
from typing import TYPE_CHECKING, Any

from jinja2 import Template
from pydantic import BaseModel
from rich.table import Table

from anta import RICH_COLOR_PALETTE, RICH_COLOR_THEME

if TYPE_CHECKING:
import pathlib

from anta.custom_types import TestStatus
from anta.result_manager import ResultManager
from anta.result_manager.models import TestResult
Expand All @@ -27,6 +28,18 @@
class ReportTable:
"""TableReport Generate a Table based on TestResult."""

class Headers(BaseModel):
titom73 marked this conversation as resolved.
Show resolved Hide resolved
"""Headers for the table report."""

device: str = "Device"
test_case: str = "Test Name"
number_of_success: str = "# of success"
number_of_failure: str = "# of failure"
number_of_skipped: str = "# of skipped"
number_of_errors: str = "# of errors"
list_of_error_nodes: str = "List of failed or error nodes"
list_of_error_tests: str = "List of failed or error test cases"

def _split_list_to_txt_list(self, usr_list: list[str], delimiter: str | None = None) -> str:
"""Split list to multi-lines string.

Expand Down Expand Up @@ -62,9 +75,6 @@ def _build_headers(self, headers: list[str], table: Table) -> Table:
for idx, header in enumerate(headers):
if idx == 0:
table.add_column(header, justify="left", style=RICH_COLOR_PALETTE.HEADER, no_wrap=True)
elif header == "Test Name":
# We always want the full test name
table.add_column(header, justify="left", no_wrap=True)
else:
table.add_column(header, justify="left")
return table
Expand Down Expand Up @@ -135,12 +145,12 @@ def report_summary_tests(
"""
table = Table(title=title, show_lines=True)
headers = [
"Test Case",
"# of success",
"# of skipped",
"# of failure",
"# of errors",
"List of failed or error nodes",
self.Headers().test_case,
self.Headers().number_of_success,
self.Headers().number_of_skipped,
self.Headers().number_of_failure,
self.Headers().number_of_errors,
self.Headers().list_of_error_nodes,
]
table = self._build_headers(headers=headers, table=table)
for test in manager.get_tests():
Expand Down Expand Up @@ -183,12 +193,12 @@ def report_summary_devices(
"""
table = Table(title=title, show_lines=True)
headers = [
"Device",
"# of success",
"# of skipped",
"# of failure",
"# of errors",
"List of failed or error test cases",
self.Headers().device,
self.Headers().number_of_success,
self.Headers().number_of_skipped,
self.Headers().number_of_failure,
self.Headers().number_of_errors,
self.Headers().list_of_error_tests,
]
table = self._build_headers(headers=headers, table=table)
for device in manager.get_devices():
Expand Down Expand Up @@ -254,3 +264,73 @@ def render(self, data: list[dict[str, Any]], *, trim_blocks: bool = True, lstrip
template = Template(file_.read(), trim_blocks=trim_blocks, lstrip_blocks=lstrip_blocks)

return template.render({"data": data})


class ReportCsv:
titom73 marked this conversation as resolved.
Show resolved Hide resolved
"""Build a CSV report."""

class Headers(BaseModel):
"""Headers for the CSV report."""

device: str = "Device"
test_name: str = "Test Name"
test_status: str = "Test Status"
messages: str = "Message(s)"
description: str = "Test description"
categories: str = "Test category"

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
-------
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 csv_report(self, results: ResultManager, csv_filename: pathlib.Path) -> None:
"""Build CSV flle with tests results.

Args:
----
results: A ResultManager instance.
csv_filename: File path where to save CSV data.
"""

def add_line(result: TestResult) -> list[str]:
message = self._split_list_to_txt_list(result.messages) if len(result.messages) > 0 else ""
categories = ", ".join(result.categories)
return [
str(result.name),
result.test,
result.result,
message.replace("\n", "\r\n"),
result.description,
categories,
]

headers = [
self.Headers().device,
self.Headers().test_name,
self.Headers().test_status,
self.Headers().messages,
self.Headers().description,
self.Headers().categories,
]

with pathlib.Path.open(csv_filename, "w", encoding="utf-8") as csvfile:
spamwriter = csv.writer(
csvfile,
delimiter=",",
)
spamwriter.writerow(headers)
for entry in results.results:
spamwriter.writerow(add_line(entry))
90 changes: 90 additions & 0 deletions anta/reporter/csv_reporter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# 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.
"""CSV Report management for ANTA."""

# pylint: disable = too-few-public-methods
from __future__ import annotations

import csv
import logging
import pathlib
from typing import TYPE_CHECKING

from pydantic import BaseModel

if TYPE_CHECKING:
from anta.result_manager import ResultManager
from anta.result_manager.models import TestResult

logger = logging.getLogger(__name__)


class ReportCsv:
"""Build a CSV report."""

class Headers(BaseModel):
carl-baillargeon marked this conversation as resolved.
Show resolved Hide resolved
"""Headers for the CSV report."""

device: str = "Device"
test_name: str = "Test Name"
test_status: str = "Test Status"
messages: str = "Message(s)"
description: str = "Test description"
categories: str = "Test category"

def _split_list_to_txt_list(self, usr_list: list[str], delimiter: str | None = None) -> str:
"""Split list to multi-lines string.

Args:
titom73 marked this conversation as resolved.
Show resolved Hide resolved
----
usr_list (list[str]): List of string to concatenate
delimiter (str, optional): A delimiter to use to start string. Defaults to None.
titom73 marked this conversation as resolved.
Show resolved Hide resolved

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 csv_report(self, results: ResultManager, csv_filename: pathlib.Path) -> None:
"""Build CSV flle with tests results.

Args:
----
results: A ResultManager instance.
csv_filename: File path where to save CSV data.
"""

def add_line(result: TestResult) -> list[str]:
message = self._split_list_to_txt_list(result.messages) if len(result.messages) > 0 else ""
categories = ", ".join(result.categories)
return [
str(result.name),
result.test,
result.result,
message.replace("\n", "\r\n"),
result.description,
categories,
]

headers = [
self.Headers().device,
self.Headers().test_name,
self.Headers().test_status,
self.Headers().messages,
self.Headers().description,
self.Headers().categories,
]

with pathlib.Path.open(csv_filename, "w", encoding="utf-8") as csvfile:
titom73 marked this conversation as resolved.
Show resolved Hide resolved
spamwriter = csv.writer(
csvfile,
delimiter=",",
)
spamwriter.writerow(headers)
for entry in results.results:
spamwriter.writerow(add_line(entry))
Loading