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 41 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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,4 @@ tech-support/*

clab-atd-anta/*
clab-atd-anta/
test.csv
1 change: 1 addition & 0 deletions anta/cli/nrfu/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,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",
)
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
14 changes: 14 additions & 0 deletions anta/cli/nrfu/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,15 @@
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.cli.utils import ExitCode
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 +125,17 @@ 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:
"""Save results to a CSV file."""
try:
ReportCsv.generate(results=_get_result_manager(ctx), csv_filename=csv_file)
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
except OSError:
console.print(f"Failed to save CSV report to {csv_file} ❌", style="cyan")
ctx.exit(ExitCode.USAGE_ERROR)


# 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
41 changes: 26 additions & 15 deletions anta/reporter/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from __future__ import annotations

import logging
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any

from jinja2 import Template
Expand All @@ -27,6 +28,19 @@
class ReportTable:
"""TableReport Generate a Table based on TestResult."""

@dataclass()
class Headers: # pylint: disable=too-many-instance-attributes
"""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 +76,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 +146,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 +194,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
109 changes: 109 additions & 0 deletions anta/reporter/csv_reporter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# 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
from dataclasses import dataclass
from typing import TYPE_CHECKING

from anta.logger import anta_log_exception

if TYPE_CHECKING:
import pathlib

from anta.result_manager import ResultManager
from anta.result_manager.models import TestResult

logger = logging.getLogger(__name__)


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

@dataclass()
class Headers:
"""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"

@classmethod
def split_list_to_txt_list(cls, usr_list: list[str], delimiter: str = " - ") -> str:
"""Split list to multi-lines string.

Parameters
----------
usr_list: List of string to concatenate
delimiter: A delimiter to use to start string. Defaults to None.

Returns
-------
str: Multi-lines string

"""
return f"{delimiter}".join(f"{line}" for line in usr_list)

@classmethod
def convert_to_list(cls, result: TestResult) -> list[str]:
"""
Convert a TestResult into a list of string for creating file content.

Args:
----
results: A TestResult to convert into list.
"""
message = cls.split_list_to_txt_list(result.messages) if len(result.messages) > 0 else ""
categories = cls.split_list_to_txt_list(result.categories) if len(result.categories) > 0 else "None"
return [
str(result.name),
result.test,
result.result,
message,
result.description,
categories,
]

@classmethod
def generate(cls, results: ResultManager, csv_filename: pathlib.Path) -> None:
"""Build CSV flle with tests results.

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

Raise
-----
OSError if any is raised while writing the CSV file.
"""
headers = [
cls.Headers.device,
cls.Headers.test_name,
cls.Headers.test_status,
cls.Headers.messages,
cls.Headers.description,
cls.Headers.categories,
]

try:
with csv_filename.open(mode="w", encoding="utf-8") as csvfile:
csvwriter = csv.writer(
csvfile,
delimiter=",",
)
csvwriter.writerow(headers)
for entry in results.results:
csvwriter.writerow(cls.convert_to_list(entry))
except OSError as exc:
message = f"OSError caught while writing the CSV file '{csv_filename.resolve()}'."
anta_log_exception(exc, message, logger)
raise
22 changes: 22 additions & 0 deletions docs/cli/nrfu.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,28 @@ anta nrfu --tags LEAF json
```
![$1anta nrfu json results](../imgs/anta-nrfu-json-output.png){ loading=lazy width="1600" }

## Performing NRFU and saving results in a CSV file.

The `csv` command in NRFU testing is useful for generating a CSV file with all tests result. This file can be easily analyzed and filtered by operator for reporting purposes.

### Command overview

```bash
anta nrfu csv --help
Usage: anta nrfu csv [OPTIONS]

ANTA command to check network states with CSV result.

Options:
--csv-output FILE Path to save report as a CSV file [env var:
ANTA_NRFU_CSV_CSV_OUTPUT]
--help Show this message and exit.
```

### Example

![anta nrfu csv results](../imgs/anta_nrfu_csv.png){ loading=lazy width="1600" }

## Performing NRFU with custom reports

ANTA offers a CLI option for creating custom reports. This leverages the Jinja2 template system, allowing you to tailor reports to your specific needs.
Expand Down
Binary file added docs/imgs/anta_nrfu_csv.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion docs/snippets/anta_nrfu_help.txt
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ Options:
or 1 if any test failed. [env var:
ANTA_NRFU_IGNORE_ERROR]
--hide [success|failure|error|skipped]
Hide result by type: success / failure /
Hide results by type: success / failure /
error / skipped'.
--dry-run Run anta nrfu command but stop before
starting to execute the tests. Considers all
Expand All @@ -52,6 +52,7 @@ Options:
--help Show this message and exit.

Commands:
csv ANTA command to check network state with CSV report.
json ANTA command to check network state with JSON result.
table ANTA command to check network states with table result.
text ANTA command to check network states with text result.
Expand Down
16 changes: 16 additions & 0 deletions tests/units/cli/nrfu/test_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import re
from pathlib import Path
from typing import TYPE_CHECKING
from unittest.mock import patch

from anta.cli import anta
from anta.cli.utils import ExitCode
Expand Down Expand Up @@ -94,3 +95,18 @@ def test_anta_nrfu_template(click_runner: CliRunner) -> None:
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


def test_anta_nrfu_csv(click_runner: CliRunner) -> None:
"""Test anta nrfu csv."""
result = click_runner.invoke(anta, ["nrfu", "csv", "--csv-output", "test.csv"])
assert result.exit_code == ExitCode.OK
assert "CSV report saved to" in result.output


def test_anta_nrfu_csv_failure(click_runner: CliRunner) -> None:
"""Test anta nrfu csv."""
with patch("anta.reporter.csv_reporter.ReportCsv.generate", side_effect=OSError()):
result = click_runner.invoke(anta, ["nrfu", "csv", "--csv-output", "read_test.csv"])
assert result.exit_code == ExitCode.USAGE_ERROR
assert "Failed to save CSV report to" in result.output
Loading
Loading