Skip to content

Markdown Reporter

Markdown report generator for ANTA test results.

STATUS_MAP module-attribute

STATUS_MAP = {
    SUCCESS: "✅ Success",
    FAILURE: "❌ Failure",
    ERROR: "❗ Error",
    SKIPPED: "⏭️ Skipped",
    UNSET: "Unset",
}

Mapping of AntaTestStatus to their string representation with icons and non-breaking spaces for Markdown.

ANTAReport

ANTAReport(
    mdfile: TextIO,
    results: ResultManager,
    extra_data: dict[str, Any] | None = None,
)

Bases: MDReportBase

Generate the # ANTA Report section of the markdown report.

Set the proper TOC to the toc attribute depending if extra_data is provided.

generate_section

generate_section() -> None

Generate the # ANTA Report section of the markdown report.

Source code in anta/reporter/md_reporter.py
344
345
346
347
def generate_section(self) -> None:
    """Generate the `# ANTA Report` section of the markdown report."""
    self.write_heading(heading_level=1)
    self.mdfile.write(self.toc + "\n\n")

MDReportBase

MDReportBase(
    mdfile: TextIO,
    results: ResultManager,
    extra_data: dict[str, Any] | None = None,
)

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
extra_data dict[str, Any] | None

Optional extra data dictionary. Can be used by subclasses to render additional data.

None

ICON class-attribute instance-attribute

ICON: str = ''

Optional icon to prepend to the section header.

format_snake_case_to_title_case

format_snake_case_to_title_case(value: str) -> str

Format a snake_case string to a Title Cased string with spaces, handling known network protocol or feature acronyms.

Parameters:

Name Type Description Default
value str

A string value to be formatted.

required

Returns:

Type Description
str

The value formatted in Title Cased.

Example
  • “hello_world” becomes “Hello World”
  • “anta_version” becomes “ANTA Version”
Source code in anta/reporter/md_reporter.py
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
def format_snake_case_to_title_case(self, value: str) -> str:
    """Format a snake_case string to a Title Cased string with spaces, handling known network protocol or feature acronyms.

    Parameters
    ----------
    value
        A string value to be formatted.

    Returns
    -------
    str
        The value formatted in Title Cased.

    Example
    -------
    - "hello_world" becomes "Hello World"
    - "anta_version" becomes "ANTA Version"
    """
    if not value:
        return ""

    parts = value.split("_")
    processed_parts = []
    for part in parts:
        if part.lower() in ACRONYM_CATEGORIES:
            processed_parts.append(part.upper())
        else:
            processed_parts.append(part.capitalize())

    return " ".join(processed_parts)

format_status

format_status(status: AntaTestStatus) -> str

Format result status with icon.

Source code in anta/reporter/md_reporter.py
298
299
300
def format_status(self, status: AntaTestStatus) -> str:
    """Format result status with icon."""
    return STATUS_MAP.get(status, status.upper())

format_timedelta

format_timedelta(value: timedelta) -> str

Format a timedelta object into a human-readable string.

Handles positive timedelta values. Milliseconds are shown only if they are the sole component of a duration less than 1 second. Does not format “days”; 2 days will return 48 hours.

Parameters:

Name Type Description Default
value timedelta

The timedelta object to be formatted.

required

Returns:

Type Description
str

The timedelta object formatted to a human-readable string.

Source code in anta/reporter/md_reporter.py
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
def format_timedelta(self, value: timedelta) -> str:
    """Format a timedelta object into a human-readable string.

    Handles positive timedelta values. Milliseconds are shown only
    if they are the sole component of a duration less than 1 second.
    Does not format "days"; 2 days will return 48 hours.

    Parameters
    ----------
    value
        The timedelta object to be formatted.

    Returns
    -------
    str
        The timedelta object formatted to a human-readable string.
    """
    total_seconds = int(value.total_seconds())

    if total_seconds < 0:
        return "Invalid duration"

    if total_seconds == 0 and value.microseconds == 0:
        return "0 seconds"

    parts = []

    hours = total_seconds // 3600
    if hours > 0:
        parts.append(f"{hours} hour{'s' if hours != 1 else ''}")
    minutes = (total_seconds % 3600) // 60
    if minutes > 0:
        parts.append(f"{minutes} minute{'s' if minutes != 1 else ''}")
    seconds = total_seconds % 60
    if seconds > 0:
        parts.append(f"{seconds} second{'s' if seconds != 1 else ''}")
    milliseconds = value.microseconds // 1000
    if milliseconds > 0 and not parts and total_seconds == 0:
        parts.append(f"{milliseconds} millisecond{'s' if milliseconds != 1 else ''}")

    return ", ".join(parts) if parts else "0 seconds"

format_value

format_value(value: Any) -> str

Format different types of values for display in the report.

Handles datetime, timedelta, lists, and other types by converting them to human-readable string representations.

Handles only positive timedelta values.

Parameters:

Name Type Description Default
value Any

A value of any type to be formatted.

required

Returns:

Type Description
str

The value formatted to a human-readable string.

Example
  • datetime.now() becomes “YYYY-MM-DD HH:MM:SS.milliseconds”
  • timedelta(hours=1, minutes=5, seconds=30) becomes “1 hour, 5 minutes, 30 seconds”
  • [“item1”, “item2”] becomes “item1, item2”
  • 123 becomes “123”
Source code in anta/reporter/md_reporter.py
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
def format_value(self, value: Any) -> str:  # noqa: ANN401
    """Format different types of values for display in the report.

    Handles datetime, timedelta, lists, and other types by converting them to
    human-readable string representations.

    Handles only positive timedelta values.

    Parameters
    ----------
    value
        A value of any type to be formatted.

    Returns
    -------
    str
        The value formatted to a human-readable string.

    Example
    -------
    - datetime.now() becomes "YYYY-MM-DD HH:MM:SS.milliseconds"
    - timedelta(hours=1, minutes=5, seconds=30) becomes "1 hour, 5 minutes, 30 seconds"
    - ["item1", "item2"] becomes "item1, item2"
    - 123 becomes "123"
    """
    if isinstance(value, datetime):
        return value.isoformat(sep=" ", timespec="milliseconds")

    if isinstance(value, timedelta):
        return self.format_timedelta(value)

    if isinstance(value, list):
        return ", ".join(str(v_item) for v_item in value)

    return str(value)

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
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
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
86
87
88
89
90
91
92
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
77
78
79
80
81
82
83
84
@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)

generate_table_heading staticmethod

generate_table_heading(
    columns: list[str], align: str = ":-"
) -> list[str]

Generate a list with the table header and its alignment row.

Parameters:

Name Type Description Default
columns list[str]

List of column names to build the table header.

required
align str

Markdown alignment string (e.g., ‘:-‘, ‘:-:’, ‘-:’). Defaults to left align.

':-'

Returns:

Type Description
list[str]

A list with the table header and its alignment row.

Source code in anta/reporter/md_reporter.py
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
@staticmethod
def generate_table_heading(columns: list[str], align: str = ":-") -> list[str]:
    """Generate a list with the table header and its alignment row.

    Parameters
    ----------
    columns
        List of column names to build the table header.
    align
        Markdown alignment string (e.g., ':-', ':-:', '-:'). Defaults to left align.

    Returns
    -------
    list[str]
        A list with the table header and its alignment row.

    """
    header_row = f"| {' | '.join(columns)} |"
    alignment_row = f"| {' | '.join([align] * len(columns))} |"
    return [header_row, alignment_row]

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
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
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 ""

    # Escape pipes so they don't break tables
    text = text.replace("|", r"\|")

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

write_heading

write_heading(heading_level: int) -> None

Write a markdown heading to the markdown file.

The heading name used is the class name.

Handles adding the icon (if defined) and creating an explicit HTML anchor so TOC links work regardless of icons.

Parameters:

Name Type Description Default
heading_level int

The level of the heading (1-6).

required
Example

## [icon] Test Results Summary <a id="test-results-summary"></a>

Source code in anta/reporter/md_reporter.py
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
def write_heading(self, heading_level: int) -> None:
    """Write a markdown heading to the markdown file.

    The heading name used is the class name.

    Handles adding the icon (if defined) and creating an explicit HTML anchor so TOC links
    work regardless of icons.

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

    Example
    -------
    `## [icon] Test Results Summary <a id="test-results-summary"></a>`
    """
    # 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()

    # Calculate the anchor ID expected by the TOC (kebab-case)
    anchor_id = heading_name.lower().replace(" ", "-")

    # Construct display name with icon
    display_name = f"{self.ICON} {heading_name}" if self.ICON else heading_name

    # Write header with explicit anchor
    heading = f'{"#" * heading_level} {display_name} <a id="{anchor_id}"></a>'

    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
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
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,
    extra_data: dict[str, Any] | None = None,
) -> 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
extra_data dict[str, Any] | None

Optional extra data dictionary that can be used by the section generators to render additional data.

None
Source code in anta/reporter/md_reporter.py
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
@classmethod
def generate(cls, results: ResultManager, md_filename: Path, extra_data: dict[str, Any] | None = None) -> 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.
    extra_data
        Optional extra data dictionary that can be used by the section generators to render additional data.
    """
    try:
        with md_filename.open("w", encoding="utf-8") as mdfile:
            for section in cls.DEFAULT_SECTIONS:
                section(mdfile, results, extra_data).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,
    extra_data: dict[str, Any] | None = None,
) -> 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
extra_data dict[str, Any] | None

Optional extra data dictionary that can be used by the section generators to render additional data.

None
Source code in anta/reporter/md_reporter.py
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
@classmethod
def generate_sections(cls, sections: list[tuple[type[MDReportBase], ResultManager]], md_filename: Path, extra_data: dict[str, Any] | None = None) -> 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.
    extra_data
        Optional extra data dictionary that can be used by the section generators to render additional data.
    """
    try:
        with md_filename.open("w", encoding="utf-8") as md_file:
            for section, rm in sections:
                section(md_file, rm, extra_data).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

RunOverview

RunOverview(
    mdfile: TextIO,
    results: ResultManager,
    extra_data: dict[str, Any] | None = None,
)

Bases: MDReportBase

Generate the ## Run Overview section of the markdown report.

The extra_data dictionary containing the desired run information must be provided to the initializer to generate this section.

NOTE: If present, the _report_options key is ignored from the extra_data dictionary as it is used for other sections.

Configure the section_data attribute using extra_data if available.

generate_rows

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

Generate the rows for the run overview table.

Source code in anta/reporter/md_reporter.py
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
def generate_rows(self) -> Generator[str, None, None]:
    """Generate the rows for the run overview table."""
    for key, value in self.section_data.items():
        label = self.format_snake_case_to_title_case(key)
        row_key = f"**{label}**"

        if isinstance(value, list):
            row_value = "<br>".join([str(item) for item in value]) if value else "None"
        elif isinstance(value, dict):
            items = []
            for k, v in value.items():
                sub_label = self.format_snake_case_to_title_case(k)
                sub_val = self.format_value(v)
                items.append(f"{sub_label}: {sub_val}")
            row_value = "<br>".join(items) if items else "None"
        else:
            row_value = self.format_value(value)

        yield f"| {row_key} | {row_value} |\n"

generate_section

generate_section() -> None

Generate the ## Run Overview section of the markdown report.

Source code in anta/reporter/md_reporter.py
398
399
400
401
402
403
404
def generate_section(self) -> None:
    """Generate the `## Run Overview` section of the markdown report."""
    if not self.section_data:
        return

    self.write_heading(heading_level=2)
    self.write_table(table_heading=self.TABLE_HEADING)

SummaryTotals

SummaryTotals(
    mdfile: TextIO,
    results: ResultManager,
    extra_data: dict[str, Any] | None = None,
)

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
444
445
446
447
448
449
450
451
452
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
454
455
456
457
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,
    extra_data: dict[str, Any] | None = None,
)

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
478
479
480
481
482
483
484
485
486
487
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(convert_categories(list(stat.categories_skipped), sort=True))
        categories_failed = ", ".join(convert_categories(list(stat.categories_failed), sort=True))
        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
489
490
491
492
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,
    extra_data: dict[str, Any] | None = None,
)

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
511
512
513
514
515
516
517
518
519
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_single_category_cached(category)
        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
521
522
523
524
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,
    extra_data: dict[str, Any] | None = None,
)

Bases: MDReportBase

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

Configure the section behavior using _report_options from extra_data if available.

generate_atomic_rows

generate_atomic_rows(
    result: TestResult,
) -> Generator[str, None, None]

Generate the rows for atomic results.

Source code in anta/reporter/md_reporter.py
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
def generate_atomic_rows(self, result: TestResult) -> Generator[str, None, None]:
    """Generate the rows for atomic results."""
    total_atomic = len(result.atomic_results)

    for idx, atomic in enumerate(result.atomic_results):
        is_last = idx == total_atomic - 1
        tree_char = "└──" if is_last else "├──"

        description = self.safe_markdown(atomic.description) if atomic.description else "-"
        atomic_description_str = f"&nbsp;&nbsp;{tree_char}&nbsp;{description}"

        atomic_messages_str = self.safe_markdown("<br>".join(atomic.messages)) or "-"
        atomic_result_str = self.format_status(atomic.result) or "-"

        if self.render_custom_field:
            yield f"| | | | {atomic_description_str} | | {atomic_result_str} | {atomic_messages_str} |\n"
        else:
            yield f"| | | | {atomic_description_str} | {atomic_result_str} | {atomic_messages_str} |\n"

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
557
558
559
560
561
562
563
564
565
566
567
568
def generate_rows(self) -> Generator[str, None, None]:
    """Generate the rows of the all test results table."""
    for result in self.results.results:
        # Check if we should render this as an expanded atomic result
        is_expanded = self.expand_results and bool(result.atomic_results)

        # Generate parent row
        yield self._format_parent_row(result, is_expanded=is_expanded)

        # Generate atomic rows (if expanded)
        if is_expanded:
            yield from self.generate_atomic_rows(result)

generate_section

generate_section() -> None

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

Source code in anta/reporter/md_reporter.py
589
590
591
592
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,
    extra_data: dict[str, Any] | None = None,
)

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
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
def generate_section(self) -> None:
    """Generate the `## Test Results Summary` section of the markdown report."""
    self.write_heading(heading_level=2)

    data = self.extra_data or {}
    report_options = data.get("_report_options", {})

    # Only display the note if results are actually expanded, as that is when the discrepancy is visible.
    if report_options.get("expand_results", False):
        note = (
            ">💡 **Note:** This report was generated with **Expanded Results** enabled. "
            "The summary sections below aggregate results at the test level, "
            "so individual checks (atomic results) are not counted in these totals.\n\n"
        )
        self.mdfile.write(note)