Repository: aristanetworks/anta
Pull request #1122: refactor(anta): Refactor MDReportGenerator
Author: login: vitthalmagadum
Labels: name: rn: refactor(anta)
Created: 2025-03-25T15:22:16Z | Merged: 2025-04-24T20:48:28Z

# Description


Refactor MDReportGenerator


Diff:
diff --git a/anta/cli/nrfu/utils.py b/anta/cli/nrfu/utils.py
index 60c0d2976..6b5077bb7 100644
--- a/anta/cli/nrfu/utils.py
+++ b/anta/cli/nrfu/utils.py
@@ -65,9 +65,11 @@ def run_tests(ctx: click.Context) -> None:
         ctx.exit()
 
 
-def _get_result_manager(ctx: click.Context) -> ResultManager:
+def _get_result_manager(ctx: click.Context, *, apply_hide_filter: bool = True) -> ResultManager:
     """Get a ResultManager instance based on Click context."""
-    return ctx.obj["result_manager"].filter(ctx.obj.get("hide")) if ctx.obj.get("hide") is not None else ctx.obj["result_manager"]
+    if apply_hide_filter:
+        return ctx.obj["result_manager"].filter(ctx.obj.get("hide")) if ctx.obj.get("hide") is not None else ctx.obj["result_manager"]
+    return ctx.obj["result_manager"]
 
 
 def print_settings(
@@ -157,7 +159,10 @@ def save_markdown_report(ctx: click.Context, md_output: pathlib.Path) -> None:
         Path to save the markdown report.
     """
     try:
-        MDReportGenerator.generate(results=_get_result_manager(ctx).sort(["name", "categories", "test"]), md_filename=md_output)
+        manager = _get_result_manager(ctx, apply_hide_filter=False).sort(["name", "categories", "test"])
+        filtered_manager = _get_result_manager(ctx, apply_hide_filter=True).sort(["name", "categories", "test"])
+        sections = [(section, filtered_manager) if section.__name__ == "TestResults" else (section, manager) for section in MDReportGenerator.DEFAULT_SECTIONS]
+        MDReportGenerator.generate_sections(md_filename=md_output, sections=sections)
         console.print(f"Markdown report saved to {md_output} ✅", style="cyan")
     except OSError:
         console.print(f"Failed to save Markdown report to {md_output} ❌", style="cyan")
diff --git a/anta/reporter/md_reporter.py b/anta/reporter/md_reporter.py
index 2d2d88245..55b993191 100644
--- a/anta/reporter/md_reporter.py
+++ b/anta/reporter/md_reporter.py
@@ -21,47 +21,8 @@
 
     from anta.result_manager import ResultManager
 
-logger = logging.getLogger(__name__)
-
-
-# pylint: disable=too-few-public-methods
-class 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.
 
-    The `generate` class method will loop over all the section subclasses and call their `generate_section` method.
-    The final report will be generated in the same order as the `sections` list of the method.
-    """
-
-    @classmethod
-    def generate(cls, results: ResultManager, md_filename: Path) -> None:
-        """Generate and write the various sections of the markdown report.
-
-        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:
-                sections: list[MDReportBase] = [
-                    ANTAReport(mdfile, results),
-                    TestResultsSummary(mdfile, results),
-                    SummaryTotals(mdfile, results),
-                    SummaryTotalsDeviceUnderTest(mdfile, results),
-                    SummaryTotalsPerCategory(mdfile, results),
-                    TestResults(mdfile, results),
-                ]
-                for section in sections:
-                    section.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
+logger = logging.getLogger(__name__)
 
 
 class MDReportBase(ABC):
@@ -297,3 +258,68 @@ 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)
+
+
+# pylint: disable=too-few-public-methods
+class 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.
+    """
+
+    DEFAULT_SECTIONS: ClassVar[list[type[MDReportBase]]] = [
+        ANTAReport,
+        TestResultsSummary,
+        SummaryTotals,
+        SummaryTotalsDeviceUnderTest,
+        SummaryTotalsPerCategory,
+        TestResults,
+    ]
+
+    @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
+
+    @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
diff --git a/tests/benchmark/test_reporter.py b/tests/benchmark/test_reporter.py
index c6d0b295f..0a5b70897 100644
--- a/tests/benchmark/test_reporter.py
+++ b/tests/benchmark/test_reporter.py
@@ -67,5 +67,6 @@ def test_csv(results: ResultManager, tmp_path: Path) -> None:
 @pytest.mark.benchmark
 @pytest.mark.dependency(depends=["anta_benchmark"], scope="package")
 def test_markdown(results: ResultManager, tmp_path: Path) -> None:
-    """Benchmark MDReportGenerator.generate()."""
-    MDReportGenerator.generate(results=results, md_filename=tmp_path / "report.md")
+    """Benchmark MDReportGenerator.generate_sections()."""
+    sections = [(section, results) for section in MDReportGenerator.DEFAULT_SECTIONS]
+    MDReportGenerator.generate_sections(sections=sections, md_filename=tmp_path / "report.md")
diff --git a/tests/data/test_md_report_custom_sections.md b/tests/data/test_md_report_custom_sections.md
new file mode 100644
index 000000000..8a9ba1e5c
--- /dev/null
+++ b/tests/data/test_md_report_custom_sections.md
@@ -0,0 +1,112 @@
+# ANTA Report
+
+**Table of Contents:**
+
+- [ANTA Report](#anta-report)
+  - [Test Results Summary](#test-results-summary)
+    - [Summary Totals](#summary-totals)
+    - [Summary Totals Device Under Test](#summary-totals-device-under-test)
+    - [Summary Totals Per Category](#summary-totals-per-category)
+  - [Test Results](#test-results)
+
+## Test Results Summary
+
+### Summary Totals
+
+| Total Tests | Total Tests Success | Total Tests Skipped | Total Tests Failure | Total Tests Error |
+| ----------- | ------------------- | ------------------- | ------------------- | ------------------|
+| 30 | 4 | 9 | 15 | 2 |
+
+### Summary Totals Device Under Test
+
+| Device Under Test | Total Tests | Tests Success | Tests Skipped | Tests Failure | Tests Error | Categories Skipped | Categories Failed |
+| ------------------| ----------- | ------------- | ------------- | ------------- | ----------- | -------------------| ------------------|
+| s1-spine1 | 30 | 4 | 9 | 15 | 2 | AVT, Field Notices, Hardware, ISIS, LANZ, OSPF, PTP, Path-Selection, Profiles | AAA, BFD, BGP, Connectivity, Cvx, Interfaces, Logging, MLAG, SNMP, STUN, Security, Services, Software, System, VLAN |
+
+### Summary Totals Per Category
+
+| Test Category | Total Tests | Tests Success | Tests Skipped | Tests Failure | Tests Error |
+| ------------- | ----------- | ------------- | ------------- | ------------- | ----------- |
+| AAA | 1 | 0 | 0 | 1 | 0 |
+| AVT | 1 | 0 | 1 | 0 | 0 |
+| BFD | 1 | 0 | 0 | 1 | 0 |
+| BGP | 1 | 0 | 0 | 0 | 1 |
+| Configuration | 1 | 1 | 0 | 0 | 0 |
+| Connectivity | 1 | 0 | 0 | 1 | 0 |
+| Cvx | 1 | 0 | 0 | 0 | 1 |
+| Field Notices | 1 | 0 | 1 | 0 | 0 |
+| Hardware | 1 | 0 | 1 | 0 | 0 |
+| Interfaces | 1 | 0 | 0 | 1 | 0 |
+| ISIS | 1 | 0 | 1 | 0 | 0 |
+| LANZ | 1 | 0 | 1 | 0 | 0 |
+| Logging | 1 | 0 | 0 | 1 | 0 |
+| MLAG | 1 | 0 | 0 | 1 | 0 |
+| OSPF | 1 | 0 | 1 | 0 | 0 |
+| Path-Selection | 1 | 0 | 1 | 0 | 0 |
+| Profiles | 1 | 0 | 1 | 0 | 0 |
+| PTP | 1 | 0 | 1 | 0 | 0 |
+| Routing | 1 | 1 | 0 | 0 | 0 |
+| Security | 2 | 0 | 0 | 2 | 0 |
+| Services | 1 | 0 | 0 | 1 | 0 |
+| SNMP | 1 | 0 | 0 | 1 | 0 |
+| Software | 1 | 0 | 0 | 1 | 0 |
+| STP | 1 | 1 | 0 | 0 | 0 |
+| STUN | 2 | 0 | 0 | 2 | 0 |
+| System | 1 | 0 | 0 | 1 | 0 |
+| VLAN | 1 | 0 | 0 | 1 | 0 |
+| VXLAN | 1 | 1 | 0 | 0 | 0 |
+
+## Failed Test Results Summary
+
+| Device Under Test | Categories | Test | Description | Custom Field | Result | Messages |
+| ----------------- | ---------- | ---- | ----------- | ------------ | ------ | -------- |
+| s1-spine1 | AAA | VerifyAcctConsoleMethods | Verifies the AAA accounting console method lists for different accounting types (system, exec, commands, dot1x). | - | failure | AAA console accounting is not configured for commands, exec, system, dot1x |
+| s1-spine1 | BFD | VerifyBFDPeersHealth | Verifies the health of IPv4 BFD peers across all VRFs. | - | failure | No IPv4 BFD peers are configured for any VRF. |
+| s1-spine1 | Connectivity | VerifyLLDPNeighbors | Verifies the connection status of the specified LLDP (Link Layer Discovery Protocol) neighbors. | - | failure | Port: Ethernet1 Neighbor: DC1-SPINE1 Neighbor Port: Ethernet1 - Wrong LLDP neighbors: spine1-dc1.fun.aristanetworks.com/Ethernet3<br>Port: Ethernet2 Neighbor: DC1-SPINE2 Neighbor Port: Ethernet1 - Wrong LLDP neighbors: spine2-dc1.fun.aristanetworks.com/Ethernet3 |
+| s1-spine1 | Interfaces | VerifyIPProxyARP | Verifies if Proxy ARP is enabled. | - | failure | Interface: Ethernet1 - Proxy-ARP disabled<br>Interface: Ethernet2 - Proxy-ARP disabled |
+| s1-spine1 | Logging | VerifyLoggingHosts | Verifies logging hosts (syslog servers) for a specified VRF. | - | failure | Syslog servers 1.1.1.1, 2.2.2.2 are not configured in VRF default |
+| s1-spine1 | MLAG | VerifyMlagDualPrimary | Verifies the MLAG dual-primary detection parameters. | - | failure | Dual-primary detection is disabled |
+| s1-spine1 | Security | VerifyBannerLogin | Verifies the login banner of a device. | - | failure | Expected '# Copyright (c) 2023-2024 Arista Networks, Inc.<br># Use of this source code is governed by the Apache License 2.0<br># that can be found in the LICENSE file.<br>' as the login banner, but found '' instead. |
+| s1-spine1 | Security | VerifyBannerMotd | Verifies the motd banner of a device. | - | failure | Expected '# Copyright (c) 2023-2024 Arista Networks, Inc.<br># Use of this source code is governed by the Apache License 2.0<br># that can be found in the LICENSE file.<br>' as the motd banner, but found '' instead. |
+| s1-spine1 | Services | VerifyHostname | Verifies the hostname of a device. | - | failure | Incorrect Hostname - Expected: s1-spine1 Actual: leaf1-dc1 |
+| s1-spine1 | SNMP | VerifySnmpContact | Verifies the SNMP contact of a device. | - | failure | SNMP contact is not configured. |
+| s1-spine1 | Software | VerifyEOSVersion | Verifies the EOS version of the device. | - | failure | EOS version mismatch - Actual: 4.31.0F-33804048.4310F (engineering build) not in Expected: 4.25.4M, 4.26.1F |
+| s1-spine1 | STUN | VerifyStunClient | (Deprecated) Verifies the translation for a source address on a STUN client. | - | failure | Client 172.18.3.2 Port: 4500 - STUN client translation not found. |
+| s1-spine1 | STUN | VerifyStunClientTranslation | Verifies the translation for a source address on a STUN client. | - | failure | Client 172.18.3.2 Port: 4500 - STUN client translation not found.<br>Client 100.64.3.2 Port: 4500 - STUN client translation not found. |
+| s1-spine1 | System | VerifyNTPAssociations | Verifies the Network Time Protocol (NTP) associations. | - | failure | NTP Server: 1.1.1.1 Preferred: True Stratum: 1 - Not configured<br>NTP Server: 2.2.2.2 Preferred: False Stratum: 2 - Not configured<br>NTP Server: 3.3.3.3 Preferred: False Stratum: 2 - Not configured |
+| s1-spine1 | VLAN | VerifyDynamicVlanSource | Verifies dynamic VLAN allocation for specified VLAN sources. | - | failure | Dynamic VLAN source(s) exist but have no VLANs allocated: mlagsync |
+
+## Test Results
+
+| Device Under Test | Categories | Test | Description | Custom Field | Result | Messages |
+| ----------------- | ---------- | ---- | ----------- | ------------ | ------ | -------- |
+| s1-spine1 | AAA | VerifyAcctConsoleMethods | Verifies the AAA accounting console method lists for different accounting types (system, exec, commands, dot1x). | - | failure | AAA console accounting is not configured for commands, exec, system, dot1x |
+| s1-spine1 | AVT | VerifyAVTPathHealth | Verifies the status of all AVT paths for all VRFs. | - | skipped | VerifyAVTPathHealth test is not supported on cEOSLab. |
+| s1-spine1 | BFD | VerifyBFDPeersHealth | Verifies the health of IPv4 BFD peers across all VRFs. | - | failure | No IPv4 BFD peers are configured for any VRF. |
+| s1-spine1 | BGP | VerifyBGPAdvCommunities | Verifies that advertised communities are standard, extended and large for BGP IPv4 peer(s). | - | error | show bgp neighbors vrf all has failed: The command is only supported in the multi-agent routing protocol model., The command is only supported in the multi-agent routing protocol model., The command is only supported in the multi-agent routing protocol model., The command is only supported in the multi-agent routing protocol model. |
+| s1-spine1 | Configuration | VerifyRunningConfigDiffs | Verifies there is no difference between the running-config and the startup-config. | - | success | - |
+| s1-spine1 | Connectivity | VerifyLLDPNeighbors | Verifies the connection status of the specified LLDP (Link Layer Discovery Protocol) neighbors. | - | failure | Port: Ethernet1 Neighbor: DC1-SPINE1 Neighbor Port: Ethernet1 - Wrong LLDP neighbors: spine1-dc1.fun.aristanetworks.com/Ethernet3<br>Port: Ethernet2 Neighbor: DC1-SPINE2 Neighbor Port: Ethernet1 - Wrong LLDP neighbors: spine2-dc1.fun.aristanetworks.com/Ethernet3 |
+| s1-spine1 | Cvx | VerifyActiveCVXConnections | Verifies the number of active CVX Connections. | - | error | show cvx connections brief has failed: Unavailable command (controller not ready) (at token 2: 'connections') |
+| s1-spine1 | Field Notices | VerifyFieldNotice44Resolution | Verifies that the device is using the correct Aboot version per FN0044. | - | skipped | VerifyFieldNotice44Resolution test is not supported on cEOSLab. |
+| s1-spine1 | Hardware | VerifyTemperature | Verifies if the device temperature is within acceptable limits. | - | skipped | VerifyTemperature test is not supported on cEOSLab. |
+| s1-spine1 | Interfaces | VerifyIPProxyARP | Verifies if Proxy ARP is enabled. | - | failure | Interface: Ethernet1 - Proxy-ARP disabled<br>Interface: Ethernet2 - Proxy-ARP disabled |
+| s1-spine1 | ISIS | VerifyISISNeighborState | Verifies the health of IS-IS neighbors. | custom | skipped | IS-IS not configured |
+| s1-spine1 | LANZ | VerifyLANZ | Verifies if LANZ is enabled. | - | skipped | VerifyLANZ test is not supported on cEOSLab. |
+| s1-spine1 | Logging | VerifyLoggingHosts | Verifies logging hosts (syslog servers) for a specified VRF. | - | failure | Syslog servers 1.1.1.1, 2.2.2.2 are not configured in VRF default |
+| s1-spine1 | MLAG | VerifyMlagDualPrimary | Verifies the MLAG dual-primary detection parameters. | - | failure | Dual-primary detection is disabled |
+| s1-spine1 | OSPF | VerifyOSPFMaxLSA | Verifies all OSPF instances did not cross the maximum LSA threshold. | - | skipped | No OSPF instance found. |
+| s1-spine1 | Path-Selection | VerifyPathsHealth | Verifies the path and telemetry state of all paths under router path-selection. | - | skipped | VerifyPathsHealth test is not supported on cEOSLab. |
+| s1-spine1 | Profiles | VerifyTcamProfile | Verifies the device TCAM profile. | - | skipped | VerifyTcamProfile test is not supported on cEOSLab. |
+| s1-spine1 | PTP | VerifyPtpGMStatus | Verifies that the device is locked to a valid PTP Grandmaster. | - | skipped | VerifyPtpGMStatus test is not supported on cEOSLab. |
+| s1-spine1 | Routing | VerifyIPv4RouteNextHops | Verifies the next-hops of the IPv4 prefixes. | - | success | - |
+| s1-spine1 | Security | VerifyBannerLogin | Verifies the login banner of a device. | - | failure | Expected '# Copyright (c) 2023-2024 Arista Networks, Inc.<br># Use of this source code is governed by the Apache License 2.0<br># that can be found in the LICENSE file.<br>' as the login banner, but found '' instead. |
+| s1-spine1 | Security | VerifyBannerMotd | Verifies the motd banner of a device. | - | failure | Expected '# Copyright (c) 2023-2024 Arista Networks, Inc.<br># Use of this source code is governed by the Apache License 2.0<br># that can be found in the LICENSE file.<br>' as the motd banner, but found '' instead. |
+| s1-spine1 | Services | VerifyHostname | Verifies the hostname of a device. | - | failure | Incorrect Hostname - Expected: s1-spine1 Actual: leaf1-dc1 |
+| s1-spine1 | SNMP | VerifySnmpContact | Verifies the SNMP contact of a device. | - | failure | SNMP contact is not configured. |
+| s1-spine1 | Software | VerifyEOSVersion | Verifies the EOS version of the device. | - | failure | EOS version mismatch - Actual: 4.31.0F-33804048.4310F (engineering build) not in Expected: 4.25.4M, 4.26.1F |
+| s1-spine1 | STP | VerifySTPBlockedPorts | Verifies there is no STP blocked ports. | - | success | - |
+| s1-spine1 | STUN | VerifyStunClient | (Deprecated) Verifies the translation for a source address on a STUN client. | - | failure | Client 172.18.3.2 Port: 4500 - STUN client translation not found. |
+| s1-spine1 | STUN | VerifyStunClientTranslation | Verifies the translation for a source address on a STUN client. | - | failure | Client 172.18.3.2 Port: 4500 - STUN client translation not found.<br>Client 100.64.3.2 Port: 4500 - STUN client translation not found. |
+| s1-spine1 | System | VerifyNTPAssociations | Verifies the Network Time Protocol (NTP) associations. | - | failure | NTP Server: 1.1.1.1 Preferred: True Stratum: 1 - Not configured<br>NTP Server: 2.2.2.2 Preferred: False Stratum: 2 - Not configured<br>NTP Server: 3.3.3.3 Preferred: False Stratum: 2 - Not configured |
+| s1-spine1 | VLAN | VerifyDynamicVlanSource | Verifies dynamic VLAN allocation for specified VLAN sources. | - | failure | Dynamic VLAN source(s) exist but have no VLANs allocated: mlagsync |
+| s1-spine1 | VXLAN | VerifyVxlan1ConnSettings | Verifies the interface vxlan1 source interface and UDP port. | - | success | - |
diff --git a/tests/units/cli/nrfu/test_commands.py b/tests/units/cli/nrfu/test_commands.py
index 1910636a1..783dfae27 100644
--- a/tests/units/cli/nrfu/test_commands.py
+++ b/tests/units/cli/nrfu/test_commands.py
@@ -178,7 +178,7 @@ def test_anta_nrfu_md_report(click_runner: CliRunner, tmp_path: Path) -> None:
 def test_anta_nrfu_md_report_failure(click_runner: CliRunner, tmp_path: Path) -> None:
     """Test anta nrfu md-report failure."""
     md_output = tmp_path / "test.md"
-    with patch("anta.reporter.md_reporter.MDReportGenerator.generate", side_effect=OSError()):
+    with patch("anta.reporter.md_reporter.MDReportGenerator.generate_sections", side_effect=OSError()):
         result = click_runner.invoke(anta, ["nrfu", "md-report", "--md-output", str(md_output)])
 
     assert result.exit_code == ExitCode.USAGE_ERROR
@@ -206,5 +206,17 @@ def test_anta_nrfu_md_report_with_hide(click_runner: CliRunner, tmp_path: Path)
     total_tests = int(match.group(1))
     total_tests_success = int(match.group(2))
 
-    assert total_tests == 0
-    assert total_tests_success == 0
+    assert total_tests == 3
+    assert total_tests_success == 3
+
+    # Collecting the rows inside the Test Results section
+    row_count = 0
+    lines = content.splitlines()
+
+    idx = lines.index("## Test Results")
+
+    for line in lines[idx + 1 :]:
+        if line.startswith("|") and "---" not in line:
+            row_count += 1
+    # Reducing the row count by 1, as above conditions counts the TABLE_HEADING
+    assert (row_count - 1) == 0
diff --git a/tests/units/reporter/test_md_reporter.py b/tests/units/reporter/test_md_reporter.py
index f5d2423ec..8bb106886 100644
--- a/tests/units/reporter/test_md_reporter.py
+++ b/tests/units/reporter/test_md_reporter.py
@@ -7,15 +7,48 @@
 
 from io import StringIO
 from pathlib import Path
+from typing import TYPE_CHECKING, ClassVar
 
 import pytest
 
 from anta.reporter.md_reporter import MDReportBase, MDReportGenerator
 from anta.result_manager import ResultManager
+from anta.result_manager.models import AntaTestStatus
+from anta.tools import convert_categories
+
+if TYPE_CHECKING:
+    from collections.abc import Generator
 
 DATA_DIR: Path = Path(__file__).parent.parent.parent.resolve() / "data"
 
 
+class FailedTestResultsSummary(MDReportBase):
+    """Test-only class used for simulating behavior in unit tests.
+
+    Generates the `## Failed Test Results Summary` section of the markdown report.
+    """
+
+    TABLE_HEADING: ClassVar[list[str]] = [
+        "| Device Under Test | Categories | Test | Description | Custom Field | Result | Messages |",
+        "| ----------------- | ---------- | ---- | ----------- | ------------ | ------ | -------- |",
+    ]
+
+    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"
+            )
+
+    def generate_section(self) -> None:
+        """Generate the `## Failed Test Results Summary` section of the markdown report."""
+        self.write_heading(heading_level=2)
+        self.write_table(table_heading=self.TABLE_HEADING)
+
+
 def test_md_report_generate(tmp_path: Path, result_manager: ResultManager) -> None:
     """Test the MDReportGenerator class."""
     md_filename = tmp_path / "test.md"
@@ -35,6 +68,31 @@ def test_md_report_generate(tmp_path: Path, result_manager: ResultManager) -> No
     assert content == expected_content
 
 
+def test_md_report_generate_sections(tmp_path: Path, result_manager: ResultManager) -> None:
+    """Test the MDReportGenerator class."""
+    md_filename = tmp_path / "test.md"
+    expected_report = "test_md_report_custom_sections.md"
+    rm = result_manager.sort(sort_by=["name", "categories", "test"])
+
+    sections = [(section, rm) for section in MDReportGenerator.DEFAULT_SECTIONS]
+    # Adding custom section
+    failed_section = (FailedTestResultsSummary, rm.filter({AntaTestStatus.SUCCESS, AntaTestStatus.ERROR, AntaTestStatus.SKIPPED, AntaTestStatus.UNSET}))
+    sections.insert(-1, failed_section)
+
+    # Generate the Markdown report
+    MDReportGenerator.generate_sections(sections, md_filename)
+    assert md_filename.exists()
+
+    # Load the existing Markdown report to compare with the generated one
+    with (DATA_DIR / expected_report).open("r", encoding="utf-8") as f:
+        expected_content = f.read()
+
+    # Check the content of the Markdown file
+    content = md_filename.read_text(encoding="utf-8")
+
+    assert content == expected_content
+
+
 def test_md_report_base() -> None:
     """Test the MDReportBase class."""
 
@@ -52,3 +110,17 @@ def generate_section(self) -> None:
 
         with pytest.raises(NotImplementedError, match="Subclasses should implement this method"):
             report.generate_rows()
+
+
+def test_md_report_error(result_manager: ResultManager) -> None:
+    """Test the MDReportGenerator class to OSError to be raised."""
+    md_filename = Path("non_existent_directory/non_existent_file.md")
+    rm = result_manager.sort(sort_by=["name", "categories", "test"])
+
+    sections = [(section, rm) for section in MDReportGenerator.DEFAULT_SECTIONS]
+
+    with pytest.raises(OSError, match="No such file or directory"):
+        MDReportGenerator.generate_sections(sections, md_filename)
+
+    with pytest.raises(OSError, match="No such file or directory"):
+        MDReportGenerator.generate(result_manager, md_filename)


Reviews:
user: login: carl-baillargeon
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T15:10:10Z
user: login: carl-baillargeon
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T18:21:18Z
user: login: vitthalmagadum
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T04:56:54Z
user: login: vitthalmagadum
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T04:57:13Z
user: login: vitthalmagadum
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T14:26:09Z
user: login: vitthalmagadum
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T14:27:13Z
user: login: vitthalmagadum
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T14:27:27Z
user: login: vitthalmagadum
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T14:27:38Z
user: login: vitthalmagadum
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T14:27:50Z
user: login: vitthalmagadum
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T14:28:20Z
user: login: vitthalmagadum
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T14:28:27Z
user: login: vitthalmagadum
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T14:28:40Z
user: login: vitthalmagadum
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T14:28:53Z
user: login: vitthalmagadum
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T14:29:01Z
user: login: vitthalmagadum
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T14:29:30Z
user: login: vitthalmagadum
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T14:29:46Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T14:54:19Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T14:56:00Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T14:57:22Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T14:57:54Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T14:58:55Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T15:05:06Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T15:07:10Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T15:08:24Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T15:08:30Z
user: login: vitthalmagadum
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T15:11:37Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T15:17:01Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T15:18:18Z
user: login: vitthalmagadum
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T15:22:44Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T15:24:16Z
user: login: vitthalmagadum
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T15:29:23Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T15:32:48Z
user: login: vitthalmagadum
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T09:58:46Z
user: login: vitthalmagadum
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T09:58:57Z
user: login: vitthalmagadum
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T09:59:04Z
user: login: vitthalmagadum
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T09:59:07Z
user: login: vitthalmagadum
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T09:59:10Z
user: login: vitthalmagadum
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T09:59:14Z
user: login: vitthalmagadum
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T09:59:19Z
user: login: vitthalmagadum
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T09:59:21Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T10:38:48Z
user: login: vitthalmagadum
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T12:21:43Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T13:18:09Z
user: login: carl-baillargeon
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T13:49:43Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T13:54:33Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T13:56:21Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T14:00:00Z
user: login: vitthalmagadum
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-21T12:38:56Z
user: login: vitthalmagadum
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-21T12:39:28Z
user: login: vitthalmagadum
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-21T12:39:45Z
user: login: vitthalmagadum
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-21T12:39:53Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T13:46:45Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T19:51:50Z
user: login: vitthalmagadum
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T04:26:16Z
user: login: vitthalmagadum
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T04:34:26Z
user: login: vitthalmagadum
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T04:34:51Z
user: login: vitthalmagadum
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T04:35:01Z
user: login: vitthalmagadum
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T04:35:17Z
user: login: vitthalmagadum
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T04:35:33Z
user: login: carl-baillargeon
body: LGTM! Thanks Vitthal
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T20:47:51Z

Review comments:
diff_hunk: @@ -157,7 +157,8 @@ def save_markdown_report(ctx: click.Context, md_output: pathlib.Path) -> None:
         Path to save the markdown report.
     """
     try:
-        MDReportGenerator.generate(results=_get_result_manager(ctx).sort(["name", "categories", "test"]), md_filename=md_output)
+        report = MDReportGenerator(results=ctx.obj["result_manager"].sort(["name", "categories", "test"]), md_file=md_output, hide=ctx.obj["hide"])
+        report.generate()
path: anta/cli/nrfu/utils.py
user: login: carl-baillargeon
body: This is breaking change because you are adding required parameters to the init. Users can no longer use the `generate` class method.
created_at: 2025-03-27T14:51:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -35,31 +38,93 @@ class MDReportGenerator:
     The final report will be generated in the same order as the `sections` list of the method.
     """
 
-    @classmethod
-    def generate(cls, results: ResultManager, md_filename: Path) -> None:
-        """Generate and write the various sections of the markdown report.
-
-        Parameters
-        ----------
-        results
-            The ResultsManager instance containing all test results.
-        md_filename
-            The path to the markdown file to write the report into.
-        """
+    def __init__(self, results: ResultManager, md_file: Path, custom_section: str | AntaTestStatus | None = None, hide: str | AntaTestStatus | None = None) -> None:
path: anta/reporter/md_reporter.py
user: login: carl-baillargeon
body: We don't need the init here. I think we should just create a separate class method to generate the provided sections and keep the existing `generate` class method.
created_at: 2025-03-27T14:54:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -35,31 +38,93 @@ class MDReportGenerator:
     The final report will be generated in the same order as the `sections` list of the method.
     """
 
-    @classmethod
-    def generate(cls, results: ResultManager, md_filename: Path) -> None:
path: anta/reporter/md_reporter.py
user: login: carl-baillargeon
body: Let's keep this method as is to avoid breaking change.
created_at: 2025-03-27T14:54:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -17,12 +18,14 @@
 
 if TYPE_CHECKING:
     from collections.abc import Generator
-    from pathlib import Path
 
     from anta.result_manager import ResultManager
 
+
 logger = logging.getLogger(__name__)
 
+SECTIONS: list[str] = list(AntaTestStatus)
path: anta/reporter/md_reporter.py
user: login: carl-baillargeon
body: This should go inside `MDReportGenerator` as a class variable so we can use them externally:
```
class MDReportGenerator:

    DEFAULT_SECTIONS: tuple[type[MDReportBase], ...] = (
        ANTAReport,
        TestResultsSummary,
        SummaryTotals,
        SummaryTotalsDeviceUnderTest,
        SummaryTotalsPerCategory,
        TestResults,
    )
```

You will probably need to move `MDReportGenerator` at the bottom of the module to have the classes defined.
created_at: 2025-03-27T14:57:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -35,31 +38,93 @@ class MDReportGenerator:
     The final report will be generated in the same order as the `sections` list of the method.
     """
 
-    @classmethod
-    def generate(cls, results: ResultManager, md_filename: Path) -> None:
-        """Generate and write the various sections of the markdown report.
-
-        Parameters
-        ----------
-        results
-            The ResultsManager instance containing all test results.
-        md_filename
-            The path to the markdown file to write the report into.
-        """
+    def __init__(self, results: ResultManager, md_file: Path, custom_section: str | AntaTestStatus | None = None, hide: str | AntaTestStatus | None = None) -> None:
+        """Initialize a MDReportGenerator instance."""
+        self.results = results
+        self.md_file = md_file
+        self._custom_section = custom_section
+        self._hide = hide
+        self.file_obj = Path.open(self.md_file, "w", encoding="utf-8")  # pylint: disable=consider-using-with
+
+    @property
+    def custom_section(self) -> Any:  # noqa: ANN401
+        """Return the custom result section to the report."""
+        if self._custom_section is None:
+            return self._custom_section
+        if isinstance(self._custom_section, set) and self._custom_section.issubset(set(SECTIONS)):
+            return self._custom_section
+        if not isinstance(self._custom_section, AntaTestStatus):
+            if self._custom_section not in SECTIONS:
+                msg = f"Invalid value for 'custom_section': '{self._custom_section}' is not one of {SECTIONS}."
+                raise ValueError(msg)
+            return {self._custom_section}
+        return self._custom_section
+
+    @custom_section.setter
+    def custom_section(self, custom_section: AntaTestStatus) -> None:
+        """Set the custom section attached to the result."""
+        self._custom_section = custom_section
+
+    @property
+    def hide(self) -> Any:  # noqa: ANN401
+        """Return the hide to filter the test results in TestResults section."""
+        if self._hide is None:
+            return self._hide
+        if isinstance(self._hide, set) and self._hide.issubset(set(SECTIONS)):
+            return self._hide
+        if not isinstance(self._hide, AntaTestStatus):
+            if self._hide not in SECTIONS:
+                msg = f"Invalid value for 'hide': '{self._hide}' is not one of {SECTIONS}."
+                raise ValueError(msg)
+            return {self._hide}
+        return self._hide
+
+    @hide.setter
+    def hide(self, hide: AntaTestStatus) -> None:
+        """Set the hide."""
+        self._hide = hide
+
+    @property
+    def sections(self) -> list[MDReportBase]:
+        """TODO: Need to review."""
+        filtered_result = self.results
+        if self.hide:
+            filtered_result = self.results.filter(self.hide).sort(["name", "categories", "test"])
+        default_sections: list[MDReportBase] = [
+            ANTAReport(self.file_obj, self.results),
+            TestResultsSummary(self.file_obj, self.results),
+            SummaryTotals(self.file_obj, self.results),
+            SummaryTotalsDeviceUnderTest(self.file_obj, self.results),
+            SummaryTotalsPerCategory(self.file_obj, self.results),
+            TestResults(self.file_obj, filtered_result),
+        ]
+
+        if self.custom_section:
+            custom_sections = self.generate_custom_section()
+            default_sections.extend(custom_sections)
+
+        return default_sections
+
+    def generate_custom_section(self) -> list[MDReportBase]:
path: anta/reporter/md_reporter.py
user: login: carl-baillargeon
body: Let's create another class method like this:
```
@classmethod
def generate_sections(cls, md_filename: Path, sections: list[tuple[type[MDReportBase], ResultManager]]) -> None:
```
With the same logic as the existing `generate`.

This allows the user to provide its own sections and managers.
created_at: 2025-03-27T15:00:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -71,7 +136,7 @@ class MDReportBase(ABC):
     to generate and write content to the provided markdown file.
     """
 
-    def __init__(self, mdfile: TextIO, results: ResultManager) -> None:
+    def __init__(self, mdfile: TextIO, results: ResultManager, heading_ow: str | None = None) -> None:
path: anta/reporter/md_reporter.py
user: login: carl-baillargeon
body: I don't think the heading_ow will be necessary since users can provide their own sections meaning their own names right?
created_at: 2025-03-27T15:01:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -35,31 +38,93 @@ class MDReportGenerator:
     The final report will be generated in the same order as the `sections` list of the method.
     """
 
-    @classmethod
-    def generate(cls, results: ResultManager, md_filename: Path) -> None:
-        """Generate and write the various sections of the markdown report.
-
-        Parameters
-        ----------
-        results
-            The ResultsManager instance containing all test results.
-        md_filename
-            The path to the markdown file to write the report into.
-        """
+    def __init__(self, results: ResultManager, md_file: Path, custom_section: str | AntaTestStatus | None = None, hide: str | AntaTestStatus | None = None) -> None:
+        """Initialize a MDReportGenerator instance."""
+        self.results = results
+        self.md_file = md_file
+        self._custom_section = custom_section
+        self._hide = hide
+        self.file_obj = Path.open(self.md_file, "w", encoding="utf-8")  # pylint: disable=consider-using-with
+
+    @property
+    def custom_section(self) -> Any:  # noqa: ANN401
+        """Return the custom result section to the report."""
+        if self._custom_section is None:
+            return self._custom_section
+        if isinstance(self._custom_section, set) and self._custom_section.issubset(set(SECTIONS)):
+            return self._custom_section
+        if not isinstance(self._custom_section, AntaTestStatus):
+            if self._custom_section not in SECTIONS:
+                msg = f"Invalid value for 'custom_section': '{self._custom_section}' is not one of {SECTIONS}."
+                raise ValueError(msg)
+            return {self._custom_section}
+        return self._custom_section
+
+    @custom_section.setter
+    def custom_section(self, custom_section: AntaTestStatus) -> None:
+        """Set the custom section attached to the result."""
+        self._custom_section = custom_section
+
+    @property
+    def hide(self) -> Any:  # noqa: ANN401
+        """Return the hide to filter the test results in TestResults section."""
+        if self._hide is None:
+            return self._hide
+        if isinstance(self._hide, set) and self._hide.issubset(set(SECTIONS)):
+            return self._hide
+        if not isinstance(self._hide, AntaTestStatus):
+            if self._hide not in SECTIONS:
+                msg = f"Invalid value for 'hide': '{self._hide}' is not one of {SECTIONS}."
+                raise ValueError(msg)
+            return {self._hide}
+        return self._hide
+
+    @hide.setter
+    def hide(self, hide: AntaTestStatus) -> None:
+        """Set the hide."""
+        self._hide = hide
+
+    @property
+    def sections(self) -> list[MDReportBase]:
+        """TODO: Need to review."""
+        filtered_result = self.results
+        if self.hide:
+            filtered_result = self.results.filter(self.hide).sort(["name", "categories", "test"])
+        default_sections: list[MDReportBase] = [
+            ANTAReport(self.file_obj, self.results),
+            TestResultsSummary(self.file_obj, self.results),
+            SummaryTotals(self.file_obj, self.results),
+            SummaryTotalsDeviceUnderTest(self.file_obj, self.results),
+            SummaryTotalsPerCategory(self.file_obj, self.results),
+            TestResults(self.file_obj, filtered_result),
+        ]
+
+        if self.custom_section:
+            custom_sections = self.generate_custom_section()
+            default_sections.extend(custom_sections)
+
+        return default_sections
+
+    def generate_custom_section(self) -> list[MDReportBase]:
+        """TODO: Need to review this. We can have a list of custom sections to add."""
+        custom_sections: list[MDReportBase] = []
+        if self.custom_section:
+            for section in [self.custom_section]:
+                filtered_result = self.results.filter(set(AntaTestStatus) - section)
+                custom_sections.append(TestResults(self.file_obj, filtered_result, heading_ow=f"{section} Result Summary"))
+
+        return custom_sections
+
+    def generate(self) -> None:
path: anta/reporter/md_reporter.py
user: login: carl-baillargeon
body: Not required. Let's just go with:
`generate_sections` class method --> New (see my other comment)
`generate` class method --> Existing, no change to avoid breaking change
created_at: 2025-03-27T15:02:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -157,7 +157,8 @@ def save_markdown_report(ctx: click.Context, md_output: pathlib.Path) -> None:
         Path to save the markdown report.
     """
     try:
-        MDReportGenerator.generate(results=_get_result_manager(ctx).sort(["name", "categories", "test"]), md_filename=md_output)
+        report = MDReportGenerator(results=ctx.obj["result_manager"].sort(["name", "categories", "test"]), md_file=md_output, hide=ctx.obj["hide"])
+        report.generate()
path: anta/cli/nrfu/utils.py
user: login: carl-baillargeon
body: So if we use the new `generate_sections`, it would look like this:
```
        manager = _get_result_manager(ctx, hide=False).sort(["name", "categories", "test"])
        hidden_manager = _get_result_manager(ctx, hide=True).sort(["name", "categories", "test"])
        sections = [(section, hidden_manager) if section.__name__ == "TestResults" else (section, manager) for section in MDReportGenerator.DEFAULT_SECTIONS]
        MDReportGenerator.generate_sections(md_filename=md_output, sections=sections)
```

With that in place, you don't need to carry the hide stuff inside the `MDReportGenerator` since you will already have a filtered manager just for the `TestResults` section. The other section will have the "full" manager which is what we want.

It also gives us the option in AVD to provide its own section(s).

You can add a flag to the `_get_result_manager` function of the CLI module to consider the `hide` flag or not.

created_at: 2025-03-27T15:08:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -35,31 +38,93 @@ class MDReportGenerator:
     The final report will be generated in the same order as the `sections` list of the method.
     """
 
-    @classmethod
-    def generate(cls, results: ResultManager, md_filename: Path) -> None:
-        """Generate and write the various sections of the markdown report.
-
-        Parameters
-        ----------
-        results
-            The ResultsManager instance containing all test results.
-        md_filename
-            The path to the markdown file to write the report into.
-        """
+    def __init__(self, results: ResultManager, md_file: Path, custom_section: str | AntaTestStatus | None = None, hide: str | AntaTestStatus | None = None) -> None:
+        """Initialize a MDReportGenerator instance."""
+        self.results = results
+        self.md_file = md_file
+        self._custom_section = custom_section
+        self._hide = hide
+        self.file_obj = Path.open(self.md_file, "w", encoding="utf-8")  # pylint: disable=consider-using-with
+
+    @property
+    def custom_section(self) -> Any:  # noqa: ANN401
+        """Return the custom result section to the report."""
+        if self._custom_section is None:
+            return self._custom_section
+        if isinstance(self._custom_section, set) and self._custom_section.issubset(set(SECTIONS)):
+            return self._custom_section
+        if not isinstance(self._custom_section, AntaTestStatus):
+            if self._custom_section not in SECTIONS:
+                msg = f"Invalid value for 'custom_section': '{self._custom_section}' is not one of {SECTIONS}."
+                raise ValueError(msg)
+            return {self._custom_section}
+        return self._custom_section
+
+    @custom_section.setter
+    def custom_section(self, custom_section: AntaTestStatus) -> None:
+        """Set the custom section attached to the result."""
+        self._custom_section = custom_section
+
+    @property
+    def hide(self) -> Any:  # noqa: ANN401
+        """Return the hide to filter the test results in TestResults section."""
+        if self._hide is None:
+            return self._hide
+        if isinstance(self._hide, set) and self._hide.issubset(set(SECTIONS)):
+            return self._hide
+        if not isinstance(self._hide, AntaTestStatus):
+            if self._hide not in SECTIONS:
+                msg = f"Invalid value for 'hide': '{self._hide}' is not one of {SECTIONS}."
+                raise ValueError(msg)
+            return {self._hide}
+        return self._hide
+
+    @hide.setter
+    def hide(self, hide: AntaTestStatus) -> None:
+        """Set the hide."""
+        self._hide = hide
+
+    @property
+    def sections(self) -> list[MDReportBase]:
+        """TODO: Need to review."""
+        filtered_result = self.results
+        if self.hide:
+            filtered_result = self.results.filter(self.hide).sort(["name", "categories", "test"])
+        default_sections: list[MDReportBase] = [
+            ANTAReport(self.file_obj, self.results),
+            TestResultsSummary(self.file_obj, self.results),
+            SummaryTotals(self.file_obj, self.results),
+            SummaryTotalsDeviceUnderTest(self.file_obj, self.results),
+            SummaryTotalsPerCategory(self.file_obj, self.results),
+            TestResults(self.file_obj, filtered_result),
+        ]
+
+        if self.custom_section:
+            custom_sections = self.generate_custom_section()
+            default_sections.extend(custom_sections)
+
+        return default_sections
path: anta/reporter/md_reporter.py
user: login: carl-baillargeon
body: We probably don't need any of this anymore with the suggestions I made.
created_at: 2025-03-27T15:09:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -134,6 +95,9 @@ def write_table(self, table_heading: list[str], *, last_table: bool = False) ->
         last_table
             Flag to determine if it's the last table of the markdown file to avoid unnecessary new line. Defaults to False.
         """
+        if len(self.results) == 0:
+            self.mdfile.write("\nNo record found.\n")
+            return
path: anta/reporter/md_reporter.py
user: login: carl-baillargeon
body: Why are you adding this?
created_at: 2025-04-07T18:09:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -65,9 +65,11 @@ def run_tests(ctx: click.Context) -> None:
         ctx.exit()
 
 
-def _get_result_manager(ctx: click.Context) -> ResultManager:
+def _get_result_manager(ctx: click.Context, *, hide: bool = True) -> ResultManager:
path: anta/cli/nrfu/utils.py
user: login: carl-baillargeon
body: Let's be more explicit:
```suggestion
def _get_result_manager(ctx: click.Context, *, apply_hide_filter: bool = True) -> ResultManager:
```
created_at: 2025-04-07T18:13:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -157,7 +159,10 @@ def save_markdown_report(ctx: click.Context, md_output: pathlib.Path) -> None:
         Path to save the markdown report.
     """
     try:
-        MDReportGenerator.generate(results=_get_result_manager(ctx).sort(["name", "categories", "test"]), md_filename=md_output)
+        manager = _get_result_manager(ctx, hide=False).sort(["name", "categories", "test"])
+        hidden_manager = _get_result_manager(ctx, hide=True).sort(["name", "categories", "test"])
path: anta/cli/nrfu/utils.py
user: login: carl-baillargeon
body: ```suggestion
        filtered_manager = _get_result_manager(ctx, hide=True).sort(["name", "categories", "test"])
```
created_at: 2025-04-07T18:13:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -297,3 +261,44 @@ 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)
+
+
+# pylint: disable=too-few-public-methods
+class 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.
+
+    The `generate` class method will loop over all the section subclasses and call their `generate_section` method.
+    The final report will be generated in the same order as the `sections` list of the method.
+    """
+
+    DEFAULT_SECTIONS: ClassVar[list[type[MDReportBase]]] = [
+        ANTAReport,
+        TestResultsSummary,
+        SummaryTotals,
+        SummaryTotalsDeviceUnderTest,
+        SummaryTotalsPerCategory,
+        TestResults,
+    ]
+
+    @classmethod
+    def generate(cls, sections: list[tuple[type[MDReportBase], ResultManager]], md_filename: Path) -> None:
path: anta/reporter/md_reporter.py
user: login: carl-baillargeon
body: This is breaking change. We need 2 methods:
1- `def generate(cls, results: ResultManager, md_filename: Path) -> None:`
2- `def generate_sections(cls, sections: list[tuple[type[MDReportBase], ResultManager]], md_filename: Path) -> None:`

Please refer to my previous intial comments on the PR. Thanks
created_at: 2025-04-07T18:19:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -206,5 +206,5 @@ def test_anta_nrfu_md_report_with_hide(click_runner: CliRunner, tmp_path: Path)
     total_tests = int(match.group(1))
     total_tests_success = int(match.group(2))
 
-    assert total_tests == 0
-    assert total_tests_success == 0
+    assert total_tests == 3
+    assert total_tests_success == 3
path: tests/units/cli/nrfu/test_commands.py
user: login: carl-baillargeon
body: Why is this changing?
created_at: 2025-04-07T18:19:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -21,8 +21,10 @@ def test_md_report_generate(tmp_path: Path, result_manager: ResultManager) -> No
     md_filename = tmp_path / "test.md"
     expected_report = "test_md_report.md"
 
+    sections = [(section, result_manager.sort(sort_by=["name", "categories", "test"])) for section in MDReportGenerator.DEFAULT_SECTIONS]
path: tests/units/reporter/test_md_reporter.py
user: login: carl-baillargeon
body: Here you should create and sort your manager first and reuse the same object in your list comprehension.
created_at: 2025-04-07T18:21:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -206,5 +206,5 @@ def test_anta_nrfu_md_report_with_hide(click_runner: CliRunner, tmp_path: Path)
     total_tests = int(match.group(1))
     total_tests_success = int(match.group(2))
 
-    assert total_tests == 0
-    assert total_tests_success == 0
+    assert total_tests == 3
+    assert total_tests_success == 3
path: tests/units/cli/nrfu/test_commands.py
user: login: vitthalmagadum
body: Its --hide unit test. Previously, the test count was managed by the filtered result manager. However, we are now separating the result managers into two distinct ones: the original and the filtered.
created_at: 2025-04-08T04:56:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -134,6 +95,9 @@ def write_table(self, table_heading: list[str], *, last_table: bool = False) ->
         last_table
             Flag to determine if it's the last table of the markdown file to avoid unnecessary new line. Defaults to False.
         """
+        if len(self.results) == 0:
+            self.mdfile.write("\nNo record found.\n")
+            return
path: anta/reporter/md_reporter.py
user: login: vitthalmagadum
body: In case we do not have any entry, the table headings will be printed. To address this, I have added a message indicating 'No record found' in the report section instead.
created_at: 2025-04-08T04:57:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -157,7 +157,8 @@ def save_markdown_report(ctx: click.Context, md_output: pathlib.Path) -> None:
         Path to save the markdown report.
     """
     try:
-        MDReportGenerator.generate(results=_get_result_manager(ctx).sort(["name", "categories", "test"]), md_filename=md_output)
+        report = MDReportGenerator(results=ctx.obj["result_manager"].sort(["name", "categories", "test"]), md_file=md_output, hide=ctx.obj["hide"])
+        report.generate()
path: anta/cli/nrfu/utils.py
user: login: vitthalmagadum
body: Updated.
created_at: 2025-04-08T14:26:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -35,31 +38,93 @@ class MDReportGenerator:
     The final report will be generated in the same order as the `sections` list of the method.
     """
 
-    @classmethod
-    def generate(cls, results: ResultManager, md_filename: Path) -> None:
-        """Generate and write the various sections of the markdown report.
-
-        Parameters
-        ----------
-        results
-            The ResultsManager instance containing all test results.
-        md_filename
-            The path to the markdown file to write the report into.
-        """
+    def __init__(self, results: ResultManager, md_file: Path, custom_section: str | AntaTestStatus | None = None, hide: str | AntaTestStatus | None = None) -> None:
path: anta/reporter/md_reporter.py
user: login: vitthalmagadum
body: Added seperate generate_sections class method.
created_at: 2025-04-08T14:27:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -35,31 +38,93 @@ class MDReportGenerator:
     The final report will be generated in the same order as the `sections` list of the method.
     """
 
-    @classmethod
-    def generate(cls, results: ResultManager, md_filename: Path) -> None:
path: anta/reporter/md_reporter.py
user: login: vitthalmagadum
body: Reverted this. Thanks!
created_at: 2025-04-08T14:27:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -17,12 +18,14 @@
 
 if TYPE_CHECKING:
     from collections.abc import Generator
-    from pathlib import Path
 
     from anta.result_manager import ResultManager
 
+
 logger = logging.getLogger(__name__)
 
+SECTIONS: list[str] = list(AntaTestStatus)
path: anta/reporter/md_reporter.py
user: login: vitthalmagadum
body: Updated. Thanks!!
created_at: 2025-04-08T14:27:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -35,31 +38,93 @@ class MDReportGenerator:
     The final report will be generated in the same order as the `sections` list of the method.
     """
 
-    @classmethod
-    def generate(cls, results: ResultManager, md_filename: Path) -> None:
-        """Generate and write the various sections of the markdown report.
-
-        Parameters
-        ----------
-        results
-            The ResultsManager instance containing all test results.
-        md_filename
-            The path to the markdown file to write the report into.
-        """
+    def __init__(self, results: ResultManager, md_file: Path, custom_section: str | AntaTestStatus | None = None, hide: str | AntaTestStatus | None = None) -> None:
+        """Initialize a MDReportGenerator instance."""
+        self.results = results
+        self.md_file = md_file
+        self._custom_section = custom_section
+        self._hide = hide
+        self.file_obj = Path.open(self.md_file, "w", encoding="utf-8")  # pylint: disable=consider-using-with
+
+    @property
+    def custom_section(self) -> Any:  # noqa: ANN401
+        """Return the custom result section to the report."""
+        if self._custom_section is None:
+            return self._custom_section
+        if isinstance(self._custom_section, set) and self._custom_section.issubset(set(SECTIONS)):
+            return self._custom_section
+        if not isinstance(self._custom_section, AntaTestStatus):
+            if self._custom_section not in SECTIONS:
+                msg = f"Invalid value for 'custom_section': '{self._custom_section}' is not one of {SECTIONS}."
+                raise ValueError(msg)
+            return {self._custom_section}
+        return self._custom_section
+
+    @custom_section.setter
+    def custom_section(self, custom_section: AntaTestStatus) -> None:
+        """Set the custom section attached to the result."""
+        self._custom_section = custom_section
+
+    @property
+    def hide(self) -> Any:  # noqa: ANN401
+        """Return the hide to filter the test results in TestResults section."""
+        if self._hide is None:
+            return self._hide
+        if isinstance(self._hide, set) and self._hide.issubset(set(SECTIONS)):
+            return self._hide
+        if not isinstance(self._hide, AntaTestStatus):
+            if self._hide not in SECTIONS:
+                msg = f"Invalid value for 'hide': '{self._hide}' is not one of {SECTIONS}."
+                raise ValueError(msg)
+            return {self._hide}
+        return self._hide
+
+    @hide.setter
+    def hide(self, hide: AntaTestStatus) -> None:
+        """Set the hide."""
+        self._hide = hide
+
+    @property
+    def sections(self) -> list[MDReportBase]:
+        """TODO: Need to review."""
+        filtered_result = self.results
+        if self.hide:
+            filtered_result = self.results.filter(self.hide).sort(["name", "categories", "test"])
+        default_sections: list[MDReportBase] = [
+            ANTAReport(self.file_obj, self.results),
+            TestResultsSummary(self.file_obj, self.results),
+            SummaryTotals(self.file_obj, self.results),
+            SummaryTotalsDeviceUnderTest(self.file_obj, self.results),
+            SummaryTotalsPerCategory(self.file_obj, self.results),
+            TestResults(self.file_obj, filtered_result),
+        ]
+
+        if self.custom_section:
+            custom_sections = self.generate_custom_section()
+            default_sections.extend(custom_sections)
+
+        return default_sections
+
+    def generate_custom_section(self) -> list[MDReportBase]:
path: anta/reporter/md_reporter.py
user: login: vitthalmagadum
body: Added.
created_at: 2025-04-08T14:27:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -71,7 +136,7 @@ class MDReportBase(ABC):
     to generate and write content to the provided markdown file.
     """
 
-    def __init__(self, mdfile: TextIO, results: ResultManager) -> None:
+    def __init__(self, mdfile: TextIO, results: ResultManager, heading_ow: str | None = None) -> None:
path: anta/reporter/md_reporter.py
user: login: vitthalmagadum
body: Yes. We can add seperate sub class for these.
created_at: 2025-04-08T14:28:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -35,31 +38,93 @@ class MDReportGenerator:
     The final report will be generated in the same order as the `sections` list of the method.
     """
 
-    @classmethod
-    def generate(cls, results: ResultManager, md_filename: Path) -> None:
-        """Generate and write the various sections of the markdown report.
-
-        Parameters
-        ----------
-        results
-            The ResultsManager instance containing all test results.
-        md_filename
-            The path to the markdown file to write the report into.
-        """
+    def __init__(self, results: ResultManager, md_file: Path, custom_section: str | AntaTestStatus | None = None, hide: str | AntaTestStatus | None = None) -> None:
+        """Initialize a MDReportGenerator instance."""
+        self.results = results
+        self.md_file = md_file
+        self._custom_section = custom_section
+        self._hide = hide
+        self.file_obj = Path.open(self.md_file, "w", encoding="utf-8")  # pylint: disable=consider-using-with
+
+    @property
+    def custom_section(self) -> Any:  # noqa: ANN401
+        """Return the custom result section to the report."""
+        if self._custom_section is None:
+            return self._custom_section
+        if isinstance(self._custom_section, set) and self._custom_section.issubset(set(SECTIONS)):
+            return self._custom_section
+        if not isinstance(self._custom_section, AntaTestStatus):
+            if self._custom_section not in SECTIONS:
+                msg = f"Invalid value for 'custom_section': '{self._custom_section}' is not one of {SECTIONS}."
+                raise ValueError(msg)
+            return {self._custom_section}
+        return self._custom_section
+
+    @custom_section.setter
+    def custom_section(self, custom_section: AntaTestStatus) -> None:
+        """Set the custom section attached to the result."""
+        self._custom_section = custom_section
+
+    @property
+    def hide(self) -> Any:  # noqa: ANN401
+        """Return the hide to filter the test results in TestResults section."""
+        if self._hide is None:
+            return self._hide
+        if isinstance(self._hide, set) and self._hide.issubset(set(SECTIONS)):
+            return self._hide
+        if not isinstance(self._hide, AntaTestStatus):
+            if self._hide not in SECTIONS:
+                msg = f"Invalid value for 'hide': '{self._hide}' is not one of {SECTIONS}."
+                raise ValueError(msg)
+            return {self._hide}
+        return self._hide
+
+    @hide.setter
+    def hide(self, hide: AntaTestStatus) -> None:
+        """Set the hide."""
+        self._hide = hide
+
+    @property
+    def sections(self) -> list[MDReportBase]:
+        """TODO: Need to review."""
+        filtered_result = self.results
+        if self.hide:
+            filtered_result = self.results.filter(self.hide).sort(["name", "categories", "test"])
+        default_sections: list[MDReportBase] = [
+            ANTAReport(self.file_obj, self.results),
+            TestResultsSummary(self.file_obj, self.results),
+            SummaryTotals(self.file_obj, self.results),
+            SummaryTotalsDeviceUnderTest(self.file_obj, self.results),
+            SummaryTotalsPerCategory(self.file_obj, self.results),
+            TestResults(self.file_obj, filtered_result),
+        ]
+
+        if self.custom_section:
+            custom_sections = self.generate_custom_section()
+            default_sections.extend(custom_sections)
+
+        return default_sections
+
+    def generate_custom_section(self) -> list[MDReportBase]:
+        """TODO: Need to review this. We can have a list of custom sections to add."""
+        custom_sections: list[MDReportBase] = []
+        if self.custom_section:
+            for section in [self.custom_section]:
+                filtered_result = self.results.filter(set(AntaTestStatus) - section)
+                custom_sections.append(TestResults(self.file_obj, filtered_result, heading_ow=f"{section} Result Summary"))
+
+        return custom_sections
+
+    def generate(self) -> None:
path: anta/reporter/md_reporter.py
user: login: vitthalmagadum
body: Updated.
created_at: 2025-04-08T14:28:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -35,31 +38,93 @@ class MDReportGenerator:
     The final report will be generated in the same order as the `sections` list of the method.
     """
 
-    @classmethod
-    def generate(cls, results: ResultManager, md_filename: Path) -> None:
-        """Generate and write the various sections of the markdown report.
-
-        Parameters
-        ----------
-        results
-            The ResultsManager instance containing all test results.
-        md_filename
-            The path to the markdown file to write the report into.
-        """
+    def __init__(self, results: ResultManager, md_file: Path, custom_section: str | AntaTestStatus | None = None, hide: str | AntaTestStatus | None = None) -> None:
+        """Initialize a MDReportGenerator instance."""
+        self.results = results
+        self.md_file = md_file
+        self._custom_section = custom_section
+        self._hide = hide
+        self.file_obj = Path.open(self.md_file, "w", encoding="utf-8")  # pylint: disable=consider-using-with
+
+    @property
+    def custom_section(self) -> Any:  # noqa: ANN401
+        """Return the custom result section to the report."""
+        if self._custom_section is None:
+            return self._custom_section
+        if isinstance(self._custom_section, set) and self._custom_section.issubset(set(SECTIONS)):
+            return self._custom_section
+        if not isinstance(self._custom_section, AntaTestStatus):
+            if self._custom_section not in SECTIONS:
+                msg = f"Invalid value for 'custom_section': '{self._custom_section}' is not one of {SECTIONS}."
+                raise ValueError(msg)
+            return {self._custom_section}
+        return self._custom_section
+
+    @custom_section.setter
+    def custom_section(self, custom_section: AntaTestStatus) -> None:
+        """Set the custom section attached to the result."""
+        self._custom_section = custom_section
+
+    @property
+    def hide(self) -> Any:  # noqa: ANN401
+        """Return the hide to filter the test results in TestResults section."""
+        if self._hide is None:
+            return self._hide
+        if isinstance(self._hide, set) and self._hide.issubset(set(SECTIONS)):
+            return self._hide
+        if not isinstance(self._hide, AntaTestStatus):
+            if self._hide not in SECTIONS:
+                msg = f"Invalid value for 'hide': '{self._hide}' is not one of {SECTIONS}."
+                raise ValueError(msg)
+            return {self._hide}
+        return self._hide
+
+    @hide.setter
+    def hide(self, hide: AntaTestStatus) -> None:
+        """Set the hide."""
+        self._hide = hide
+
+    @property
+    def sections(self) -> list[MDReportBase]:
+        """TODO: Need to review."""
+        filtered_result = self.results
+        if self.hide:
+            filtered_result = self.results.filter(self.hide).sort(["name", "categories", "test"])
+        default_sections: list[MDReportBase] = [
+            ANTAReport(self.file_obj, self.results),
+            TestResultsSummary(self.file_obj, self.results),
+            SummaryTotals(self.file_obj, self.results),
+            SummaryTotalsDeviceUnderTest(self.file_obj, self.results),
+            SummaryTotalsPerCategory(self.file_obj, self.results),
+            TestResults(self.file_obj, filtered_result),
+        ]
+
+        if self.custom_section:
+            custom_sections = self.generate_custom_section()
+            default_sections.extend(custom_sections)
+
+        return default_sections
path: anta/reporter/md_reporter.py
user: login: vitthalmagadum
body: Removed this part.
created_at: 2025-04-08T14:28:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -65,9 +65,11 @@ def run_tests(ctx: click.Context) -> None:
         ctx.exit()
 
 
-def _get_result_manager(ctx: click.Context) -> ResultManager:
+def _get_result_manager(ctx: click.Context, *, hide: bool = True) -> ResultManager:
path: anta/cli/nrfu/utils.py
user: login: vitthalmagadum
body: Updated
created_at: 2025-04-08T14:28:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -157,7 +159,10 @@ def save_markdown_report(ctx: click.Context, md_output: pathlib.Path) -> None:
         Path to save the markdown report.
     """
     try:
-        MDReportGenerator.generate(results=_get_result_manager(ctx).sort(["name", "categories", "test"]), md_filename=md_output)
+        manager = _get_result_manager(ctx, hide=False).sort(["name", "categories", "test"])
+        hidden_manager = _get_result_manager(ctx, hide=True).sort(["name", "categories", "test"])
path: anta/cli/nrfu/utils.py
user: login: vitthalmagadum
body: Updated.
created_at: 2025-04-08T14:29:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -297,3 +261,44 @@ 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)
+
+
+# pylint: disable=too-few-public-methods
+class 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.
+
+    The `generate` class method will loop over all the section subclasses and call their `generate_section` method.
+    The final report will be generated in the same order as the `sections` list of the method.
+    """
+
+    DEFAULT_SECTIONS: ClassVar[list[type[MDReportBase]]] = [
+        ANTAReport,
+        TestResultsSummary,
+        SummaryTotals,
+        SummaryTotalsDeviceUnderTest,
+        SummaryTotalsPerCategory,
+        TestResults,
+    ]
+
+    @classmethod
+    def generate(cls, sections: list[tuple[type[MDReportBase], ResultManager]], md_filename: Path) -> None:
path: anta/reporter/md_reporter.py
user: login: vitthalmagadum
body: Reverted this change. Added seperate method.
created_at: 2025-04-08T14:29:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -21,8 +21,10 @@ def test_md_report_generate(tmp_path: Path, result_manager: ResultManager) -> No
     md_filename = tmp_path / "test.md"
     expected_report = "test_md_report.md"
 
+    sections = [(section, result_manager.sort(sort_by=["name", "categories", "test"])) for section in MDReportGenerator.DEFAULT_SECTIONS]
path: tests/units/reporter/test_md_reporter.py
user: login: vitthalmagadum
body: updated.
created_at: 2025-04-08T14:29:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -134,6 +95,9 @@ def write_table(self, table_heading: list[str], *, last_table: bool = False) ->
         last_table
             Flag to determine if it's the last table of the markdown file to avoid unnecessary new line. Defaults to False.
         """
+        if len(self.results) == 0:
+            self.mdfile.write("\nNo record found.\n")
+            return
path: anta/reporter/md_reporter.py
user: login: carl-baillargeon
body: Can you show me a screenshot please :)
created_at: 2025-04-10T14:54:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -297,3 +261,72 @@ 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)
+
+
+# pylint: disable=too-few-public-methods
+class 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.
+
+    The `generate` class method will loop over all the section subclasses and call their `generate_section` method.
+    The final report will be generated in the same order as the `sections` list of the method.
+    """
+
+    DEFAULT_SECTIONS: ClassVar[list[type[MDReportBase]]] = [
+        ANTAReport,
+        TestResultsSummary,
+        SummaryTotals,
+        SummaryTotalsDeviceUnderTest,
+        SummaryTotalsPerCategory,
+        TestResults,
+    ]
+
+    @classmethod
+    def generate(cls, results: ResultManager, md_filename: Path) -> None:
+        """Generate and write the various sections of the markdown report.
+
+        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:
+                sections: list[MDReportBase] = [
+                    ANTAReport(mdfile, results),
+                    TestResultsSummary(mdfile, results),
+                    SummaryTotals(mdfile, results),
+                    SummaryTotalsDeviceUnderTest(mdfile, results),
+                    SummaryTotalsPerCategory(mdfile, results),
+                    TestResults(mdfile, results),
+                ]
+                for section in sections:
path: anta/reporter/md_reporter.py
user: login: carl-baillargeon
body: Here you can use DEFAULT_SECTIONS instead.
created_at: 2025-04-10T14:56:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -297,3 +261,72 @@ 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)
+
+
+# pylint: disable=too-few-public-methods
+class 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.
+
+    The `generate` class method will loop over all the section subclasses and call their `generate_section` method.
+    The final report will be generated in the same order as the `sections` list of the method.
+    """
path: anta/reporter/md_reporter.py
user: login: carl-baillargeon
body: We should update this and say there are 2 different methods to generate the report, how they differ, etc.
created_at: 2025-04-10T14:57:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -297,3 +261,72 @@ 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)
+
+
+# pylint: disable=too-few-public-methods
+class 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.
+
+    The `generate` class method will loop over all the section subclasses and call their `generate_section` method.
+    The final report will be generated in the same order as the `sections` list of the method.
+    """
+
+    DEFAULT_SECTIONS: ClassVar[list[type[MDReportBase]]] = [
+        ANTAReport,
+        TestResultsSummary,
+        SummaryTotals,
+        SummaryTotalsDeviceUnderTest,
+        SummaryTotalsPerCategory,
+        TestResults,
+    ]
+
+    @classmethod
+    def generate(cls, results: ResultManager, md_filename: Path) -> None:
+        """Generate and write the various sections of the markdown report.
path: anta/reporter/md_reporter.py
user: login: carl-baillargeon
body: I would specify that we generate the default section using the same manager.
created_at: 2025-04-10T14:57:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -297,3 +261,72 @@ 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)
+
+
+# pylint: disable=too-few-public-methods
+class 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.
+
+    The `generate` class method will loop over all the section subclasses and call their `generate_section` method.
+    The final report will be generated in the same order as the `sections` list of the method.
+    """
+
+    DEFAULT_SECTIONS: ClassVar[list[type[MDReportBase]]] = [
+        ANTAReport,
+        TestResultsSummary,
+        SummaryTotals,
+        SummaryTotalsDeviceUnderTest,
+        SummaryTotalsPerCategory,
+        TestResults,
+    ]
+
+    @classmethod
+    def generate(cls, results: ResultManager, md_filename: Path) -> None:
+        """Generate and write the various sections of the markdown report.
+
+        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:
+                sections: list[MDReportBase] = [
+                    ANTAReport(mdfile, results),
+                    TestResultsSummary(mdfile, results),
+                    SummaryTotals(mdfile, results),
+                    SummaryTotalsDeviceUnderTest(mdfile, results),
+                    SummaryTotalsPerCategory(mdfile, results),
+                    TestResults(mdfile, results),
+                ]
+                for section in sections:
+                    section.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
+
+    @classmethod
+    def generate_sections(cls, sections: list[tuple[type[MDReportBase], ResultManager]], md_filename: Path) -> None:
+        """Generate and write the various sections of the markdown report.
path: anta/reporter/md_reporter.py
user: login: carl-baillargeon
body: We should add a little bit more details here saying that each section is generated with its own manager (or something like that).
created_at: 2025-04-10T14:58:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -68,4 +68,6 @@ def test_csv(results: ResultManager, tmp_path: Path) -> None:
 @pytest.mark.dependency(depends=["anta_benchmark"], scope="package")
 def test_markdown(results: ResultManager, tmp_path: Path) -> None:
     """Benchmark MDReportGenerator.generate()."""
-    MDReportGenerator.generate(results=results, md_filename=tmp_path / "report.md")
+    rm = results.sort(sort_by=["name", "categories", "test"])
+    sections = [(section, rm) for section in MDReportGenerator.DEFAULT_SECTIONS]
+    MDReportGenerator.generate_sections(sections=sections, md_filename=tmp_path / "report.md")
path: tests/benchmark/test_reporter.py
user: login: carl-baillargeon
body: I guess this is why CI (Codspeed) is complaining about a performance decrease right? Can you investigate why it's slower than the existing `generate`?
created_at: 2025-04-10T15:05:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -206,5 +206,5 @@ def test_anta_nrfu_md_report_with_hide(click_runner: CliRunner, tmp_path: Path)
     total_tests = int(match.group(1))
     total_tests_success = int(match.group(2))
 
-    assert total_tests == 0
-    assert total_tests_success == 0
+    assert total_tests == 3
+    assert total_tests_success == 3
path: tests/units/cli/nrfu/test_commands.py
user: login: carl-baillargeon
body: In that case we need to check the Test Results section to make sure they are hidden from there but not in the Total Tests Success.
created_at: 2025-04-10T15:07:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -52,3 +74,17 @@ def generate_section(self) -> None:
 
         with pytest.raises(NotImplementedError, match="Subclasses should implement this method"):
             report.generate_rows()
+
+
+def test_md_report_error(result_manager: ResultManager) -> None:
+    """Test the MDReportGenerator class to OSError to be raised."""
+    md_filename = Path("non_existent_directory/non_existent_file.md")
+    rm = result_manager.sort(sort_by=["name", "categories", "test"])
+
+    sections = [(section, rm) for section in MDReportGenerator.DEFAULT_SECTIONS]
+
+    with pytest.raises(OSError):  # noqa: PT011
path: tests/units/reporter/test_md_reporter.py
user: login: carl-baillargeon
body: Why not use match= statisfy ruff?
created_at: 2025-04-10T15:08:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -52,3 +74,17 @@ def generate_section(self) -> None:
 
         with pytest.raises(NotImplementedError, match="Subclasses should implement this method"):
             report.generate_rows()
+
+
+def test_md_report_error(result_manager: ResultManager) -> None:
+    """Test the MDReportGenerator class to OSError to be raised."""
+    md_filename = Path("non_existent_directory/non_existent_file.md")
+    rm = result_manager.sort(sort_by=["name", "categories", "test"])
+
+    sections = [(section, rm) for section in MDReportGenerator.DEFAULT_SECTIONS]
+
+    with pytest.raises(OSError):  # noqa: PT011
+        MDReportGenerator.generate_sections(sections, md_filename)
+
+    with pytest.raises(OSError):  # noqa: PT011
path: tests/units/reporter/test_md_reporter.py
user: login: carl-baillargeon
body: Same here.
created_at: 2025-04-10T15:08:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -134,6 +95,9 @@ def write_table(self, table_heading: list[str], *, last_table: bool = False) ->
         last_table
             Flag to determine if it's the last table of the markdown file to avoid unnecessary new line. Defaults to False.
         """
+        if len(self.results) == 0:
+            self.mdfile.write("\nNo record found.\n")
+            return
path: anta/reporter/md_reporter.py
user: login: vitthalmagadum
body: Using **--hide** to hide failure tests.


<img width="1176" alt="Screenshot 2025-04-10 at 8 39 45 PM" src="https://github.com/user-attachments/assets/8dd9218a-3633-4581-baf2-039de870b0e5" />

created_at: 2025-04-10T15:11:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -134,6 +95,9 @@ def write_table(self, table_heading: list[str], *, last_table: bool = False) ->
         last_table
             Flag to determine if it's the last table of the markdown file to avoid unnecessary new line. Defaults to False.
         """
+        if len(self.results) == 0:
+            self.mdfile.write("\nNo record found.\n")
+            return
path: anta/reporter/md_reporter.py
user: login: carl-baillargeon
body: Nice perfect. Let's keep this thanks
created_at: 2025-04-10T15:17:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -134,6 +95,9 @@ def write_table(self, table_heading: list[str], *, last_table: bool = False) ->
         last_table
             Flag to determine if it's the last table of the markdown file to avoid unnecessary new line. Defaults to False.
         """
+        if len(self.results) == 0:
+            self.mdfile.write("\nNo record found.\n")
+            return
path: anta/reporter/md_reporter.py
user: login: carl-baillargeon
body: Wait... What would happen if we have no results at all? I guess the other sections would have that too right?
created_at: 2025-04-10T15:18:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -134,6 +95,9 @@ def write_table(self, table_heading: list[str], *, last_table: bool = False) ->
         last_table
             Flag to determine if it's the last table of the markdown file to avoid unnecessary new line. Defaults to False.
         """
+        if len(self.results) == 0:
+            self.mdfile.write("\nNo record found.\n")
+            return
path: anta/reporter/md_reporter.py
user: login: vitthalmagadum
body: No results mean 0 tests?
<img width="981" alt="Screenshot 2025-04-10 at 8 53 39 PM" src="https://github.com/user-attachments/assets/e8d7ab93-725b-4e9c-861f-e96b1ce246e9" />

created_at: 2025-04-10T15:22:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -134,6 +95,9 @@ def write_table(self, table_heading: list[str], *, last_table: bool = False) ->
         last_table
             Flag to determine if it's the last table of the markdown file to avoid unnecessary new line. Defaults to False.
         """
+        if len(self.results) == 0:
+            self.mdfile.write("\nNo record found.\n")
+            return
path: anta/reporter/md_reporter.py
user: login: carl-baillargeon
body: Correct 0 test. Can you test please?
created_at: 2025-04-10T15:24:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -134,6 +95,9 @@ def write_table(self, table_heading: list[str], *, last_table: bool = False) ->
         last_table
             Flag to determine if it's the last table of the markdown file to avoid unnecessary new line. Defaults to False.
         """
+        if len(self.results) == 0:
+            self.mdfile.write("\nNo record found.\n")
+            return
path: anta/reporter/md_reporter.py
user: login: vitthalmagadum
body: updated screenshot in the same
created_at: 2025-04-10T15:29:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -134,6 +95,9 @@ def write_table(self, table_heading: list[str], *, last_table: bool = False) ->
         last_table
             Flag to determine if it's the last table of the markdown file to avoid unnecessary new line. Defaults to False.
         """
+        if len(self.results) == 0:
+            self.mdfile.write("\nNo record found.\n")
+            return
path: anta/reporter/md_reporter.py
user: login: carl-baillargeon
body: Ok let's remove this code and keep the existing behavior for now. We will find a way to provide more information to the reporters once #680 is merged.
created_at: 2025-04-10T15:32:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -134,6 +95,9 @@ def write_table(self, table_heading: list[str], *, last_table: bool = False) ->
         last_table
             Flag to determine if it's the last table of the markdown file to avoid unnecessary new line. Defaults to False.
         """
+        if len(self.results) == 0:
+            self.mdfile.write("\nNo record found.\n")
+            return
path: anta/reporter/md_reporter.py
user: login: vitthalmagadum
body: Removed. Thanks!!
created_at: 2025-04-11T09:58:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -206,5 +206,5 @@ def test_anta_nrfu_md_report_with_hide(click_runner: CliRunner, tmp_path: Path)
     total_tests = int(match.group(1))
     total_tests_success = int(match.group(2))
 
-    assert total_tests == 0
-    assert total_tests_success == 0
+    assert total_tests == 3
+    assert total_tests_success == 3
path: tests/units/cli/nrfu/test_commands.py
user: login: vitthalmagadum
body: Would you like me to add a new unit test for this functionality?
@carl-baillargeon 
created_at: 2025-04-11T09:58:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -297,3 +261,72 @@ 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)
+
+
+# pylint: disable=too-few-public-methods
+class 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.
+
+    The `generate` class method will loop over all the section subclasses and call their `generate_section` method.
+    The final report will be generated in the same order as the `sections` list of the method.
+    """
+
+    DEFAULT_SECTIONS: ClassVar[list[type[MDReportBase]]] = [
+        ANTAReport,
+        TestResultsSummary,
+        SummaryTotals,
+        SummaryTotalsDeviceUnderTest,
+        SummaryTotalsPerCategory,
+        TestResults,
+    ]
+
+    @classmethod
+    def generate(cls, results: ResultManager, md_filename: Path) -> None:
+        """Generate and write the various sections of the markdown report.
+
+        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:
+                sections: list[MDReportBase] = [
+                    ANTAReport(mdfile, results),
+                    TestResultsSummary(mdfile, results),
+                    SummaryTotals(mdfile, results),
+                    SummaryTotalsDeviceUnderTest(mdfile, results),
+                    SummaryTotalsPerCategory(mdfile, results),
+                    TestResults(mdfile, results),
+                ]
+                for section in sections:
path: anta/reporter/md_reporter.py
user: login: vitthalmagadum
body: Updated
created_at: 2025-04-11T09:59:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -297,3 +261,72 @@ 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)
+
+
+# pylint: disable=too-few-public-methods
+class 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.
+
+    The `generate` class method will loop over all the section subclasses and call their `generate_section` method.
+    The final report will be generated in the same order as the `sections` list of the method.
+    """
path: anta/reporter/md_reporter.py
user: login: vitthalmagadum
body: Updated
created_at: 2025-04-11T09:59:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -297,3 +261,72 @@ 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)
+
+
+# pylint: disable=too-few-public-methods
+class 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.
+
+    The `generate` class method will loop over all the section subclasses and call their `generate_section` method.
+    The final report will be generated in the same order as the `sections` list of the method.
+    """
+
+    DEFAULT_SECTIONS: ClassVar[list[type[MDReportBase]]] = [
+        ANTAReport,
+        TestResultsSummary,
+        SummaryTotals,
+        SummaryTotalsDeviceUnderTest,
+        SummaryTotalsPerCategory,
+        TestResults,
+    ]
+
+    @classmethod
+    def generate(cls, results: ResultManager, md_filename: Path) -> None:
+        """Generate and write the various sections of the markdown report.
path: anta/reporter/md_reporter.py
user: login: vitthalmagadum
body: Updated
created_at: 2025-04-11T09:59:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -297,3 +261,72 @@ 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)
+
+
+# pylint: disable=too-few-public-methods
+class 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.
+
+    The `generate` class method will loop over all the section subclasses and call their `generate_section` method.
+    The final report will be generated in the same order as the `sections` list of the method.
+    """
+
+    DEFAULT_SECTIONS: ClassVar[list[type[MDReportBase]]] = [
+        ANTAReport,
+        TestResultsSummary,
+        SummaryTotals,
+        SummaryTotalsDeviceUnderTest,
+        SummaryTotalsPerCategory,
+        TestResults,
+    ]
+
+    @classmethod
+    def generate(cls, results: ResultManager, md_filename: Path) -> None:
+        """Generate and write the various sections of the markdown report.
+
+        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:
+                sections: list[MDReportBase] = [
+                    ANTAReport(mdfile, results),
+                    TestResultsSummary(mdfile, results),
+                    SummaryTotals(mdfile, results),
+                    SummaryTotalsDeviceUnderTest(mdfile, results),
+                    SummaryTotalsPerCategory(mdfile, results),
+                    TestResults(mdfile, results),
+                ]
+                for section in sections:
+                    section.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
+
+    @classmethod
+    def generate_sections(cls, sections: list[tuple[type[MDReportBase], ResultManager]], md_filename: Path) -> None:
+        """Generate and write the various sections of the markdown report.
path: anta/reporter/md_reporter.py
user: login: vitthalmagadum
body: Updated
created_at: 2025-04-11T09:59:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -52,3 +74,17 @@ def generate_section(self) -> None:
 
         with pytest.raises(NotImplementedError, match="Subclasses should implement this method"):
             report.generate_rows()
+
+
+def test_md_report_error(result_manager: ResultManager) -> None:
+    """Test the MDReportGenerator class to OSError to be raised."""
+    md_filename = Path("non_existent_directory/non_existent_file.md")
+    rm = result_manager.sort(sort_by=["name", "categories", "test"])
+
+    sections = [(section, rm) for section in MDReportGenerator.DEFAULT_SECTIONS]
+
+    with pytest.raises(OSError):  # noqa: PT011
path: tests/units/reporter/test_md_reporter.py
user: login: vitthalmagadum
body: Updated
created_at: 2025-04-11T09:59:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -52,3 +74,17 @@ def generate_section(self) -> None:
 
         with pytest.raises(NotImplementedError, match="Subclasses should implement this method"):
             report.generate_rows()
+
+
+def test_md_report_error(result_manager: ResultManager) -> None:
+    """Test the MDReportGenerator class to OSError to be raised."""
+    md_filename = Path("non_existent_directory/non_existent_file.md")
+    rm = result_manager.sort(sort_by=["name", "categories", "test"])
+
+    sections = [(section, rm) for section in MDReportGenerator.DEFAULT_SECTIONS]
+
+    with pytest.raises(OSError):  # noqa: PT011
+        MDReportGenerator.generate_sections(sections, md_filename)
+
+    with pytest.raises(OSError):  # noqa: PT011
path: tests/units/reporter/test_md_reporter.py
user: login: vitthalmagadum
body: Updated
created_at: 2025-04-11T09:59:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -206,5 +206,5 @@ def test_anta_nrfu_md_report_with_hide(click_runner: CliRunner, tmp_path: Path)
     total_tests = int(match.group(1))
     total_tests_success = int(match.group(2))
 
-    assert total_tests == 0
-    assert total_tests_success == 0
+    assert total_tests == 3
+    assert total_tests_success == 3
path: tests/units/cli/nrfu/test_commands.py
user: login: carl-baillargeon
body: Yes please. 
created_at: 2025-04-11T10:38:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -68,4 +68,6 @@ def test_csv(results: ResultManager, tmp_path: Path) -> None:
 @pytest.mark.dependency(depends=["anta_benchmark"], scope="package")
 def test_markdown(results: ResultManager, tmp_path: Path) -> None:
     """Benchmark MDReportGenerator.generate()."""
-    MDReportGenerator.generate(results=results, md_filename=tmp_path / "report.md")
+    rm = results.sort(sort_by=["name", "categories", "test"])
+    sections = [(section, rm) for section in MDReportGenerator.DEFAULT_SECTIONS]
+    MDReportGenerator.generate_sections(sections=sections, md_filename=tmp_path / "report.md")
path: tests/benchmark/test_reporter.py
user: login: vitthalmagadum
body: IMO: We're currently using two sorted result managers, and since sorting can be time-consuming, it's contributing to performance overhead. Additionally, fetching class constructors via a list lookup is further impacting code execution speed.
created_at: 2025-04-11T12:21:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -68,4 +68,6 @@ def test_csv(results: ResultManager, tmp_path: Path) -> None:
 @pytest.mark.dependency(depends=["anta_benchmark"], scope="package")
 def test_markdown(results: ResultManager, tmp_path: Path) -> None:
     """Benchmark MDReportGenerator.generate()."""
-    MDReportGenerator.generate(results=results, md_filename=tmp_path / "report.md")
+    rm = results.sort(sort_by=["name", "categories", "test"])
+    sections = [(section, rm) for section in MDReportGenerator.DEFAULT_SECTIONS]
+    MDReportGenerator.generate_sections(sections=sections, md_filename=tmp_path / "report.md")
path: tests/benchmark/test_reporter.py
user: login: carl-baillargeon
body: Ok I will investigate
created_at: 2025-04-14T13:18:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -297,3 +258,70 @@ 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)
+
+
+# pylint: disable=too-few-public-methods
+class 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. The sections are
+    processed in the order defined by the `DEFAULT_SECTIONS` list, and the same manager is passed to each
+    section's `generate_section` method.
+
+    - `generate_sections`: Each section is generated using its own dedicated result manager instance. This allows for
+    greater flexibility or isolation between section generations.
path: anta/reporter/md_reporter.py
user: login: carl-baillargeon
body: ```suggestion
    - `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.
```
created_at: 2025-04-14T13:45:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -35,6 +35,28 @@ def test_md_report_generate(tmp_path: Path, result_manager: ResultManager) -> No
     assert content == expected_content
 
 
+def test_md_report_generate_sections(tmp_path: Path, result_manager: ResultManager) -> None:
+    """Test the MDReportGenerator class."""
+    md_filename = tmp_path / "test.md"
+    expected_report = "test_md_report.md"
+    rm = result_manager.sort(sort_by=["name", "categories", "test"])
+
+    sections = [(section, rm) for section in MDReportGenerator.DEFAULT_SECTIONS]
path: tests/units/reporter/test_md_reporter.py
user: login: carl-baillargeon
body: We also need a unit test with custom sections mixed with default sections to make sure the order/rendering is fine.
created_at: 2025-04-14T13:49:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -68,4 +68,6 @@ def test_csv(results: ResultManager, tmp_path: Path) -> None:
 @pytest.mark.dependency(depends=["anta_benchmark"], scope="package")
 def test_markdown(results: ResultManager, tmp_path: Path) -> None:
     """Benchmark MDReportGenerator.generate()."""
path: tests/benchmark/test_reporter.py
user: login: carl-baillargeon
body: ```suggestion
    """Benchmark MDReportGenerator.generate_sections()."""
```
created_at: 2025-04-14T13:54:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -68,4 +68,6 @@ def test_csv(results: ResultManager, tmp_path: Path) -> None:
 @pytest.mark.dependency(depends=["anta_benchmark"], scope="package")
 def test_markdown(results: ResultManager, tmp_path: Path) -> None:
     """Benchmark MDReportGenerator.generate()."""
-    MDReportGenerator.generate(results=results, md_filename=tmp_path / "report.md")
+    rm = results.sort(sort_by=["name", "categories", "test"])
+    sections = [(section, rm) for section in MDReportGenerator.DEFAULT_SECTIONS]
+    MDReportGenerator.generate_sections(sections=sections, md_filename=tmp_path / "report.md")
path: tests/benchmark/test_reporter.py
user: login: carl-baillargeon
body: We were not sorting before so let's remove sort here. It's a benchmark of `generate_sections` only so no need to benchmark sorting.
created_at: 2025-04-14T13:56:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -35,6 +35,28 @@ def test_md_report_generate(tmp_path: Path, result_manager: ResultManager) -> No
     assert content == expected_content
 
 
+def test_md_report_generate_sections(tmp_path: Path, result_manager: ResultManager) -> None:
+    """Test the MDReportGenerator class."""
+    md_filename = tmp_path / "test.md"
+    expected_report = "test_md_report.md"
+    rm = result_manager.sort(sort_by=["name", "categories", "test"])
+
+    sections = [(section, rm) for section in MDReportGenerator.DEFAULT_SECTIONS]
path: tests/units/reporter/test_md_reporter.py
user: login: carl-baillargeon
body: You could probably create `FailedTestResultsSummary` section similar to what we have in AVD for testing.


created_at: 2025-04-14T13:59:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -206,5 +206,5 @@ def test_anta_nrfu_md_report_with_hide(click_runner: CliRunner, tmp_path: Path)
     total_tests = int(match.group(1))
     total_tests_success = int(match.group(2))
 
-    assert total_tests == 0
-    assert total_tests_success == 0
+    assert total_tests == 3
+    assert total_tests_success == 3
path: tests/units/cli/nrfu/test_commands.py
user: login: vitthalmagadum
body: We have 3 existing unit tests (test_anta_nrfu_md_report, test_anta_nrfu_md_report_failure, test_anta_nrfu_md_report_with_hide)
created_at: 2025-04-21T12:38:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -297,3 +258,70 @@ 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)
+
+
+# pylint: disable=too-few-public-methods
+class 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. The sections are
+    processed in the order defined by the `DEFAULT_SECTIONS` list, and the same manager is passed to each
+    section's `generate_section` method.
+
+    - `generate_sections`: Each section is generated using its own dedicated result manager instance. This allows for
+    greater flexibility or isolation between section generations.
path: anta/reporter/md_reporter.py
user: login: vitthalmagadum
body: Updated. Thanks!!
created_at: 2025-04-21T12:39:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -35,6 +35,28 @@ def test_md_report_generate(tmp_path: Path, result_manager: ResultManager) -> No
     assert content == expected_content
 
 
+def test_md_report_generate_sections(tmp_path: Path, result_manager: ResultManager) -> None:
+    """Test the MDReportGenerator class."""
+    md_filename = tmp_path / "test.md"
+    expected_report = "test_md_report.md"
+    rm = result_manager.sort(sort_by=["name", "categories", "test"])
+
+    sections = [(section, rm) for section in MDReportGenerator.DEFAULT_SECTIONS]
path: tests/units/reporter/test_md_reporter.py
user: login: vitthalmagadum
body: Added unit test for custom section.
created_at: 2025-04-21T12:39:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -68,4 +68,6 @@ def test_csv(results: ResultManager, tmp_path: Path) -> None:
 @pytest.mark.dependency(depends=["anta_benchmark"], scope="package")
 def test_markdown(results: ResultManager, tmp_path: Path) -> None:
     """Benchmark MDReportGenerator.generate()."""
path: tests/benchmark/test_reporter.py
user: login: vitthalmagadum
body: Updated.
created_at: 2025-04-21T12:39:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -296,4 +257,93 @@ def generate_rows(self) -> Generator[str, None, None]:
     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)  # TODO: Have to find a way for last line.
+
+
+class FailedTestResultsSummary(MDReportBase):
path: anta/reporter/md_reporter.py
user: login: carl-baillargeon
body: Move this to a module in unit tests since this is where it's being used. It should not be available in the ANTA code.
created_at: 2025-04-22T13:36:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -296,4 +257,93 @@ def generate_rows(self) -> Generator[str, None, None]:
     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)  # TODO: Have to find a way for last line.
path: anta/reporter/md_reporter.py
user: login: carl-baillargeon
body: If a user wants to add a section after this one, they will need to override this section as well and set `last_table=False` so I don't think we need the TODO here, unless I'm missing something :)
created_at: 2025-04-22T13:43:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -206,5 +206,5 @@ def test_anta_nrfu_md_report_with_hide(click_runner: CliRunner, tmp_path: Path)
     total_tests = int(match.group(1))
     total_tests_success = int(match.group(2))
 
-    assert total_tests == 0
-    assert total_tests_success == 0
+    assert total_tests == 3
+    assert total_tests_success == 3
path: tests/units/cli/nrfu/test_commands.py
user: login: carl-baillargeon
body: In this specific unit test `test_anta_nrfu_md_report_with_hide`, we are currently checking the `Total Tests Success` section to make sure we have 3 tests now. We need to add another check for the `Test Results` section to make sure the success tests are effectively hidden from this section.
created_at: 2025-04-22T13:46:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -106,6 +106,7 @@ repos:
     hooks:
       - id: markdownlint
         name: Check Markdown files style.
+        exclude: ^tests/data/test_md_report_custom_sections\.md$  # If a user wants to add a section in MD report, they will need to override the section as well and set last_table=False. RE: anta/tests/units/reporter/test_md_reporter.py
path: .pre-commit-config.yaml
user: login: carl-baillargeon
body: If we insert the custom section before the Test Results section, I think it would solve this problem.
created_at: 2025-04-23T19:33:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -297,3 +258,68 @@ 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)
+
+
+# pylint: disable=too-few-public-methods
+class 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.
path: anta/reporter/md_reporter.py
user: login: carl-baillargeon
body: Does it render properly in the documentation?
created_at: 2025-04-23T19:34:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -35,6 +65,31 @@ def test_md_report_generate(tmp_path: Path, result_manager: ResultManager) -> No
     assert content == expected_content
 
 
+def test_md_report_generate_sections(tmp_path: Path, result_manager: ResultManager) -> None:
+    """Test the MDReportGenerator class."""
+    md_filename = tmp_path / "test.md"
+    expected_report = "test_md_report_custom_sections.md"
+    rm = result_manager.sort(sort_by=["name", "categories", "test"])
+
+    sections = [(section, rm) for section in MDReportGenerator.DEFAULT_SECTIONS]
+    # Adding custom section
+    failed_section = (FailedTestResultsSummary, rm.filter({AntaTestStatus.SUCCESS, AntaTestStatus.ERROR, AntaTestStatus.SKIPPED, AntaTestStatus.UNSET}))
+    sections.append(failed_section)
path: tests/units/reporter/test_md_reporter.py
user: login: carl-baillargeon
body: You can use `sections.insert()` here to insert the section before the `Test Results` section.
created_at: 2025-04-23T19:41:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -7,15 +7,45 @@
 
 from io import StringIO
 from pathlib import Path
+from typing import TYPE_CHECKING, ClassVar
 
 import pytest
 
 from anta.reporter.md_reporter import MDReportBase, MDReportGenerator
 from anta.result_manager import ResultManager
+from anta.result_manager.models import AntaTestStatus
+from anta.tools import convert_categories
+
+if TYPE_CHECKING:
+    from collections.abc import Generator
 
 DATA_DIR: Path = Path(__file__).parent.parent.parent.resolve() / "data"
 
 
+class FailedTestResultsSummary(MDReportBase):
+    """Generates the `## Failed Test Results Summary` section of the markdown report."""
path: tests/units/reporter/test_md_reporter.py
user: login: carl-baillargeon
body: Can you add a comment to specify that this is a custom section for testing purposes?
created_at: 2025-04-23T19:49:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -206,5 +206,17 @@ def test_anta_nrfu_md_report_with_hide(click_runner: CliRunner, tmp_path: Path)
     total_tests = int(match.group(1))
     total_tests_success = int(match.group(2))
 
-    assert total_tests == 0
-    assert total_tests_success == 0
+    assert total_tests == 3
+    assert total_tests_success == 3
+
+    # Collecting the row inside the Test Results section
path: tests/units/cli/nrfu/test_commands.py
user: login: carl-baillargeon
body: ```suggestion
    # Collecting the rows inside the Test Results section
```
created_at: 2025-04-23T19:50:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -206,5 +206,17 @@ def test_anta_nrfu_md_report_with_hide(click_runner: CliRunner, tmp_path: Path)
     total_tests = int(match.group(1))
     total_tests_success = int(match.group(2))
 
-    assert total_tests == 0
-    assert total_tests_success == 0
+    assert total_tests == 3
+    assert total_tests_success == 3
+
+    # Collecting the row inside the Test Results section
+    row_count = 0
+    lines = content.splitlines()
+
+    idx = lines.index("## Test Results")
+
+    for line in lines[idx + 1 :]:
+        if line.startswith("|") and "---" not in line:
+            row_count += 1
+    # Reducing the row count by 1, as above conditions counts the TABLE_HEADING
+    assert (row_count - 1) == 0
path: tests/units/cli/nrfu/test_commands.py
user: login: carl-baillargeon
body: That means when we hide success tests there is no result in the manager correct?
created_at: 2025-04-23T19:51:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -206,5 +206,17 @@ def test_anta_nrfu_md_report_with_hide(click_runner: CliRunner, tmp_path: Path)
     total_tests = int(match.group(1))
     total_tests_success = int(match.group(2))
 
-    assert total_tests == 0
-    assert total_tests_success == 0
+    assert total_tests == 3
+    assert total_tests_success == 3
+
+    # Collecting the row inside the Test Results section
+    row_count = 0
+    lines = content.splitlines()
+
+    idx = lines.index("## Test Results")
+
+    for line in lines[idx + 1 :]:
+        if line.startswith("|") and "---" not in line:
+            row_count += 1
+    # Reducing the row count by 1, as above conditions counts the TABLE_HEADING
+    assert (row_count - 1) == 0
path: tests/units/cli/nrfu/test_commands.py
user: login: vitthalmagadum
body: Yes, that's correct. We have one test in the catalog running across three different devices, and it's passing on all of them.
created_at: 2025-04-24T04:26:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -106,6 +106,7 @@ repos:
     hooks:
       - id: markdownlint
         name: Check Markdown files style.
+        exclude: ^tests/data/test_md_report_custom_sections\.md$  # If a user wants to add a section in MD report, they will need to override the section as well and set last_table=False. RE: anta/tests/units/reporter/test_md_reporter.py
path: .pre-commit-config.yaml
user: login: vitthalmagadum
body: Yes. Updated. Thanks!!
created_at: 2025-04-24T04:34:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -35,6 +65,31 @@ def test_md_report_generate(tmp_path: Path, result_manager: ResultManager) -> No
     assert content == expected_content
 
 
+def test_md_report_generate_sections(tmp_path: Path, result_manager: ResultManager) -> None:
+    """Test the MDReportGenerator class."""
+    md_filename = tmp_path / "test.md"
+    expected_report = "test_md_report_custom_sections.md"
+    rm = result_manager.sort(sort_by=["name", "categories", "test"])
+
+    sections = [(section, rm) for section in MDReportGenerator.DEFAULT_SECTIONS]
+    # Adding custom section
+    failed_section = (FailedTestResultsSummary, rm.filter({AntaTestStatus.SUCCESS, AntaTestStatus.ERROR, AntaTestStatus.SKIPPED, AntaTestStatus.UNSET}))
+    sections.append(failed_section)
path: tests/units/reporter/test_md_reporter.py
user: login: vitthalmagadum
body: Updated. Thanks!!
created_at: 2025-04-24T04:34:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -206,5 +206,17 @@ def test_anta_nrfu_md_report_with_hide(click_runner: CliRunner, tmp_path: Path)
     total_tests = int(match.group(1))
     total_tests_success = int(match.group(2))
 
-    assert total_tests == 0
-    assert total_tests_success == 0
+    assert total_tests == 3
+    assert total_tests_success == 3
+
+    # Collecting the row inside the Test Results section
path: tests/units/cli/nrfu/test_commands.py
user: login: vitthalmagadum
body: Updated.
created_at: 2025-04-24T04:35:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -7,15 +7,45 @@
 
 from io import StringIO
 from pathlib import Path
+from typing import TYPE_CHECKING, ClassVar
 
 import pytest
 
 from anta.reporter.md_reporter import MDReportBase, MDReportGenerator
 from anta.result_manager import ResultManager
+from anta.result_manager.models import AntaTestStatus
+from anta.tools import convert_categories
+
+if TYPE_CHECKING:
+    from collections.abc import Generator
 
 DATA_DIR: Path = Path(__file__).parent.parent.parent.resolve() / "data"
 
 
+class FailedTestResultsSummary(MDReportBase):
+    """Generates the `## Failed Test Results Summary` section of the markdown report."""
path: tests/units/reporter/test_md_reporter.py
user: login: vitthalmagadum
body: Updated the class docstring
created_at: 2025-04-24T04:35:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -297,3 +258,68 @@ 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)
+
+
+# pylint: disable=too-few-public-methods
+class 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.
path: anta/reporter/md_reporter.py
user: login: vitthalmagadum
body: <img width="1512" alt="Screenshot 2025-04-24 at 10 03 45 AM" src="https://github.com/user-attachments/assets/c07633c7-62bc-49ea-ba09-0fd21e4e5742" />

created_at: 2025-04-24T04:35:32Z
author_association: CONTRIBUTOR

PR comments:
user: login: codspeed-hq[bot]
created_at: 2025-03-26T17:43:21Z
body: <!-- __CODSPEED_PERFORMANCE_REPORT_COMMENT__ -->
<!-- __CODSPEED_INSTRUMENTATION_PERFORMANCE_REPORT_COMMENT__ -->

## [CodSpeed Performance Report](https://codspeed.io/aristanetworks/anta/branches/vitthalmagadum%3Afeat%2Fmd-report)

### Merging #1122 will **not alter performance**

<sub>Comparing <code>vitthalmagadum:feat/md-report</code> (79295cd) with <code>main</code> (70f0c07)</sub>



### Summary

`✅ 22` untouched benchmarks  




author_association: NONE
user: login: sonarqubecloud[bot]
created_at: 2025-04-24T04:49:54Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=aristanetworks_anta&pullRequest=1122) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=aristanetworks_anta&pullRequest=1122&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=aristanetworks_anta&pullRequest=1122&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=aristanetworks_anta&pullRequest=1122&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [100.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=aristanetworks_anta&pullRequest=1122&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=aristanetworks_anta&pullRequest=1122&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=aristanetworks_anta&pullRequest=1122)


author_association: NONE