Skip to content

Markdown Reporter

Markdown report generator for ANTA test results.

ANTAReport

ANTAReport(mdfile: TextIO, results: ResultManager)

Bases: MDReportBase

Generate the # ANTA Report section of the markdown report.

generate_section

generate_section() -> None

Generate the # ANTA Report section of the markdown report.

Source code in anta/reporter/md_reporter.py
151
152
153
154
155
def generate_section(self) -> None:
    """Generate the `# ANTA Report` section of the markdown report."""
    self.write_heading(heading_level=1)
    toc = MD_REPORT_TOC
    self.mdfile.write(toc + "\n\n")

MDReportBase

MDReportBase(mdfile: TextIO, results: ResultManager)

Bases: ABC

Base class for all sections subclasses.

Every subclasses must implement the generate_section method that uses the ResultManager object to generate and write content to the provided markdown file.

Parameters:

Name Type Description Default
mdfile TextIO

An open file object to write the markdown data into.

required
results ResultManager

The ResultsManager instance containing all test results.

required

generate_heading_name

generate_heading_name() -> str

Generate a formatted heading name based on the class name.

Returns:

Type Description
str

Formatted header name.

Example
  • ANTAReport will become ANTA Report.
  • TestResultsSummary will become Test Results Summary.
Source code in anta/reporter/md_reporter.py
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
def generate_heading_name(self) -> str:
    """Generate a formatted heading name based on the class name.

    Returns
    -------
    str
        Formatted header name.

    Example
    -------
    - `ANTAReport` will become `ANTA Report`.
    - `TestResultsSummary` will become `Test Results Summary`.
    """
    class_name = self.__class__.__name__

    # Split the class name into words, keeping acronyms together
    words = re.findall(r"[A-Z]?[a-z]+|[A-Z]+(?=[A-Z][a-z]|\d|\W|$)|\d+", class_name)

    # Capitalize each word, but keep acronyms in all caps
    formatted_words = [word if word.isupper() else word.capitalize() for word in words]

    return " ".join(formatted_words)

generate_rows

generate_rows() -> Generator[str, None, None]

Generate the rows of a markdown table for a specific report section.

Subclasses can implement this method to generate the content of the table rows.

Source code in anta/reporter/md_reporter.py
57
58
59
60
61
62
63
def generate_rows(self) -> Generator[str, None, None]:
    """Generate the rows of a markdown table for a specific report section.

    Subclasses can implement this method to generate the content of the table rows.
    """
    msg = "Subclasses should implement this method"
    raise NotImplementedError(msg)

generate_section abstractmethod

generate_section() -> None

Abstract method to generate a specific section of the markdown report.

Must be implemented by subclasses.

Source code in anta/reporter/md_reporter.py
48
49
50
51
52
53
54
55
@abstractmethod
def generate_section(self) -> None:
    """Abstract method to generate a specific section of the markdown report.

    Must be implemented by subclasses.
    """
    msg = "Must be implemented by subclasses"
    raise NotImplementedError(msg)

safe_markdown

safe_markdown(text: str | None) -> str

Escape markdown characters in the text to prevent markdown rendering issues.

Parameters:

Name Type Description Default
text str | None

The text to escape markdown characters from.

required

Returns:

Type Description
str

The text with escaped markdown characters.

Source code in anta/reporter/md_reporter.py
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
def safe_markdown(self, text: str | None) -> str:
    """Escape markdown characters in the text to prevent markdown rendering issues.

    Parameters
    ----------
    text
        The text to escape markdown characters from.

    Returns
    -------
    str
        The text with escaped markdown characters.
    """
    # Custom field from a TestResult object can be None
    if text is None:
        return ""

    # Replace newlines with <br> to preserve line breaks in HTML
    text = text.replace("\n", "<br>")

    # Replace backticks with single quotes
    return text.replace("`", "'")

write_heading

write_heading(heading_level: int) -> None

Write a markdown heading to the markdown file.

The heading name used is the class name.

Parameters:

Name Type Description Default
heading_level int

The level of the heading (1-6).

required
Example

## Test Results Summary

Source code in anta/reporter/md_reporter.py
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
def write_heading(self, heading_level: int) -> None:
    """Write a markdown heading to the markdown file.

    The heading name used is the class name.

    Parameters
    ----------
    heading_level
        The level of the heading (1-6).

    Example
    -------
    `## Test Results Summary`
    """
    # Ensure the heading level is within the valid range of 1 to 6
    heading_level = max(1, min(heading_level, 6))
    heading_name = self.generate_heading_name()
    heading = "#" * heading_level + " " + heading_name
    self.mdfile.write(f"{heading}\n\n")

write_table

write_table(
    table_heading: list[str], *, last_table: bool = False
) -> None

Write a markdown table with a table heading and multiple rows to the markdown file.

Parameters:

Name Type Description Default
table_heading list[str]

List of strings to join for the table heading.

required
last_table bool

Flag to determine if it’s the last table of the markdown file to avoid unnecessary new line. Defaults to False.

False
Source code in anta/reporter/md_reporter.py
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
def write_table(self, table_heading: list[str], *, last_table: bool = False) -> None:
    """Write a markdown table with a table heading and multiple rows to the markdown file.

    Parameters
    ----------
    table_heading
        List of strings to join for the table heading.
    last_table
        Flag to determine if it's the last table of the markdown file to avoid unnecessary new line. Defaults to False.
    """
    self.mdfile.write("\n".join(table_heading) + "\n")
    for row in self.generate_rows():
        self.mdfile.write(row)
    if not last_table:
        self.mdfile.write("\n")

MDReportGenerator

Class responsible for generating a Markdown report based on the provided ResultManager object.

It aggregates different report sections, each represented by a subclass of MDReportBase, and sequentially generates their content into a markdown file.

This class provides two methods for generating the report:

  • generate: Uses a single result manager instance to generate all sections defined in the DEFAULT_SECTIONS class variable list.

  • generate_sections: A custom list of sections is provided. Each section uses its own dedicated result manager instance, allowing greater flexibility or isolation between section generations.

generate classmethod

generate(results: ResultManager, md_filename: Path) -> None

Generate the sections of the markdown report defined in DEFAULT_SECTIONS using a single result manager instance for all sections.

Parameters:

Name Type Description Default
results ResultManager

The ResultsManager instance containing all test results.

required
md_filename Path

The path to the markdown file to write the report into.

required
Source code in anta/reporter/md_reporter.py
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
@classmethod
def generate(cls, results: ResultManager, md_filename: Path) -> None:
    """Generate the sections of the markdown report defined in DEFAULT_SECTIONS using a single result manager instance for all sections.

    Parameters
    ----------
    results
        The ResultsManager instance containing all test results.
    md_filename
        The path to the markdown file to write the report into.
    """
    try:
        with md_filename.open("w", encoding="utf-8") as mdfile:
            for section in cls.DEFAULT_SECTIONS:
                section(mdfile, results).generate_section()
    except OSError as exc:
        message = f"OSError caught while writing the Markdown file '{md_filename.resolve()}'."
        anta_log_exception(exc, message, logger)
        raise

generate_sections classmethod

generate_sections(
    sections: list[
        tuple[type[MDReportBase], ResultManager]
    ],
    md_filename: Path,
) -> None

Generate the different sections of the markdown report provided in the sections argument with each section using its own result manager instance.

Parameters:

Name Type Description Default
sections list[tuple[type[MDReportBase], ResultManager]]

A list of tuples, where each tuple contains a subclass of MDReportBase and an instance of ResultManager.

required
md_filename Path

The path to the markdown file to write the report into.

required
Source code in anta/reporter/md_reporter.py
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
@classmethod
def generate_sections(cls, sections: list[tuple[type[MDReportBase], ResultManager]], md_filename: Path) -> None:
    """Generate the different sections of the markdown report provided in the sections argument with each section using its own result manager instance.

    Parameters
    ----------
    sections
        A list of tuples, where each tuple contains a subclass of `MDReportBase` and an instance of `ResultManager`.
    md_filename
        The path to the markdown file to write the report into.
    """
    try:
        with md_filename.open("w", encoding="utf-8") as md_file:
            for section, rm in sections:
                section(md_file, rm).generate_section()
    except OSError as exc:
        message = f"OSError caught while writing the Markdown file '{md_filename.resolve()}'."
        anta_log_exception(exc, message, logger)
        raise

SummaryTotals

SummaryTotals(mdfile: TextIO, results: ResultManager)

Bases: MDReportBase

Generate the ### Summary Totals section of the markdown report.

generate_rows

generate_rows() -> Generator[str, None, None]

Generate the rows of the summary totals table.

Source code in anta/reporter/md_reporter.py
174
175
176
177
178
179
180
181
182
def generate_rows(self) -> Generator[str, None, None]:
    """Generate the rows of the summary totals table."""
    yield (
        f"| {self.results.get_total_results()} "
        f"| {self.results.get_total_results({AntaTestStatus.SUCCESS})} "
        f"| {self.results.get_total_results({AntaTestStatus.SKIPPED})} "
        f"| {self.results.get_total_results({AntaTestStatus.FAILURE})} "
        f"| {self.results.get_total_results({AntaTestStatus.ERROR})} |\n"
    )

generate_section

generate_section() -> None

Generate the ### Summary Totals section of the markdown report.

Source code in anta/reporter/md_reporter.py
184
185
186
187
def generate_section(self) -> None:
    """Generate the `### Summary Totals` section of the markdown report."""
    self.write_heading(heading_level=3)
    self.write_table(table_heading=self.TABLE_HEADING)

SummaryTotalsDeviceUnderTest

SummaryTotalsDeviceUnderTest(
    mdfile: TextIO, results: ResultManager
)

Bases: MDReportBase

Generate the ### Summary Totals Devices Under Tests section of the markdown report.

generate_rows

generate_rows() -> Generator[str, None, None]

Generate the rows of the summary totals device under test table.

Source code in anta/reporter/md_reporter.py
198
199
200
201
202
203
204
205
206
207
def generate_rows(self) -> Generator[str, None, None]:
    """Generate the rows of the summary totals device under test table."""
    for device, stat in self.results.device_stats.items():
        total_tests = stat.tests_success_count + stat.tests_skipped_count + stat.tests_failure_count + stat.tests_error_count + stat.tests_unset_count
        categories_skipped = ", ".join(sorted(convert_categories(list(stat.categories_skipped))))
        categories_failed = ", ".join(sorted(convert_categories(list(stat.categories_failed))))
        yield (
            f"| {device} | {total_tests} | {stat.tests_success_count} | {stat.tests_skipped_count} | {stat.tests_failure_count} | {stat.tests_error_count} "
            f"| {categories_skipped or '-'} | {categories_failed or '-'} |\n"
        )

generate_section

generate_section() -> None

Generate the ### Summary Totals Devices Under Tests section of the markdown report.

Source code in anta/reporter/md_reporter.py
209
210
211
212
def generate_section(self) -> None:
    """Generate the `### Summary Totals Devices Under Tests` section of the markdown report."""
    self.write_heading(heading_level=3)
    self.write_table(table_heading=self.TABLE_HEADING)

SummaryTotalsPerCategory

SummaryTotalsPerCategory(
    mdfile: TextIO, results: ResultManager
)

Bases: MDReportBase

Generate the ### Summary Totals Per Category section of the markdown report.

generate_rows

generate_rows() -> Generator[str, None, None]

Generate the rows of the summary totals per category table.

Source code in anta/reporter/md_reporter.py
223
224
225
226
227
228
229
230
231
def generate_rows(self) -> Generator[str, None, None]:
    """Generate the rows of the summary totals per category table."""
    for category, stat in self.results.category_stats.items():
        converted_category = convert_categories([category])[0]
        total_tests = stat.tests_success_count + stat.tests_skipped_count + stat.tests_failure_count + stat.tests_error_count + stat.tests_unset_count
        yield (
            f"| {converted_category} | {total_tests} | {stat.tests_success_count} | {stat.tests_skipped_count} | {stat.tests_failure_count} "
            f"| {stat.tests_error_count} |\n"
        )

generate_section

generate_section() -> None

Generate the ### Summary Totals Per Category section of the markdown report.

Source code in anta/reporter/md_reporter.py
233
234
235
236
def generate_section(self) -> None:
    """Generate the `### Summary Totals Per Category` section of the markdown report."""
    self.write_heading(heading_level=3)
    self.write_table(table_heading=self.TABLE_HEADING)

TestResults

TestResults(mdfile: TextIO, results: ResultManager)

Bases: MDReportBase

Generates the ## Test Results section of the markdown report.

generate_rows

generate_rows() -> Generator[str, None, None]

Generate the rows of the all test results table.

Source code in anta/reporter/md_reporter.py
247
248
249
250
251
252
253
254
255
def generate_rows(self) -> Generator[str, None, None]:
    """Generate the rows of the all test results table."""
    for result in self.results.results:
        messages = self.safe_markdown(result.messages[0]) if len(result.messages) == 1 else self.safe_markdown("<br>".join(result.messages))
        categories = ", ".join(sorted(convert_categories(result.categories)))
        yield (
            f"| {result.name or '-'} | {categories or '-'} | {result.test or '-'} "
            f"| {result.description or '-'} | {self.safe_markdown(result.custom_field) or '-'} | {result.result or '-'} | {messages or '-'} |\n"
        )

generate_section

generate_section() -> None

Generate the ## Test Results section of the markdown report.

Source code in anta/reporter/md_reporter.py
257
258
259
260
def generate_section(self) -> None:
    """Generate the `## Test Results` section of the markdown report."""
    self.write_heading(heading_level=2)
    self.write_table(table_heading=self.TABLE_HEADING, last_table=True)

TestResultsSummary

TestResultsSummary(mdfile: TextIO, results: ResultManager)

Bases: MDReportBase

Generate the ## Test Results Summary section of the markdown report.

generate_section

generate_section() -> None

Generate the ## Test Results Summary section of the markdown report.

Source code in anta/reporter/md_reporter.py
161
162
163
def generate_section(self) -> None:
    """Generate the `## Test Results Summary` section of the markdown report."""
    self.write_heading(heading_level=2)