Skip to content

feat: merge Robot and PyATS xunit.xml files for CI/CD integration#557

Merged
oboehmer merged 19 commits intorelease/pyats-integration-v1.1-betafrom
feature/xunit-merger
Feb 26, 2026
Merged

feat: merge Robot and PyATS xunit.xml files for CI/CD integration#557
oboehmer merged 19 commits intorelease/pyats-integration-v1.1-betafrom
feature/xunit-merger

Conversation

@oboehmer
Copy link
Collaborator

@oboehmer oboehmer commented Feb 20, 2026

Description

Implements combined xunit.xml generation by merging Robot Framework and PyATS test results into a single file at the output root directory. This enables CI/CD pipelines (Jenkins, GitLab) to consume a unified test results file.

Note: This is a stacked PR on top of #543 (hostname display tests). Please merge #543 first.
The commit to review is 14b9e38

Closes

Related Issue(s)

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Refactoring / Technical debt (internal improvements with no user-facing changes)
  • Documentation update
  • Chore (build process, CI, tooling, dependencies)
  • Other (please describe):

Test Framework Affected

  • PyATS
  • Robot Framework
  • Both
  • N/A (not test-framework specific)

Network as Code (NaC) Architecture Affected

  • ACI (APIC)
  • NDO (Nexus Dashboard Orchestrator)
  • NDFC / VXLAN-EVPN (Nexus Dashboard Fabric Controller)
  • Catalyst SD-WAN (SDWAN Manager / vManage)
  • Catalyst Center (DNA Center)
  • ISE (Identity Services Engine)
  • FMC (Firepower Management Center)
  • Meraki (Cloud-managed)
  • NX-OS (Nexus Direct-to-Device)
  • IOS-XE (Direct-to-Device)
  • IOS-XR (Direct-to-Device)
  • Hyperfabric
  • All architectures
  • N/A (architecture-agnostic)

Platform Tested

  • macOS (version tested: macOS 15.3 / Apple Silicon)
  • Linux (distro/version tested: )

Key Changes

  • Add xunit_merger module using lxml.etree for efficient XML parsing
  • Move Robot xunit.xml output to robot_results/ subdirectory (no longer at root)
  • Remove xunit.xml from backward-compat symlinks (merged file replaces it)
  • Integrate merger into combined orchestrator post-execution
  • Add lxml>=4.5.1 as direct dependency (was already indirect)
  • Add 17 unit tests for merger functionality
  • Extend all E2E test scenarios to verify merged xunit.xml creation

Testing Done

  • Unit tests added/updated
  • Integration tests performed
  • Manual testing performed:
    • PyATS tests executed successfully
    • Robot Framework tests executed successfully
    • D2D/SSH tests executed successfully (if applicable)
    • HTML reports generated correctly
  • All existing tests pass (pytest / pre-commit run -a)

Test Commands Used

uv run pre-commit run --all-files
uv run pytest tests/unit -n auto --dist loadscope  # 284 passed
uv run pytest tests/e2e -n auto --dist loadscope   # 277 passed, 90 skipped

Checklist

  • Code follows project style guidelines (pre-commit run -a passes)
  • Self-review of code completed
  • Code is commented where necessary (especially complex logic)
  • Documentation updated (if applicable)
  • No new warnings introduced
  • Changes work on both macOS and Linux
  • CHANGELOG.md updated (if applicable)

Screenshots (if applicable)

N/A

Additional Notes

Output structure change: The root xunit.xml is now a merged file (not a symlink). Individual xunit files remain in their respective subdirectories:

  • robot_results/xunit.xml - Robot Framework results
  • pyats_results/api/xunit.xml - PyATS API results
  • pyats_results/d2d/<hostname>/xunit.xml - PyATS D2D results per device

Why lxml? Selected over stdlib xml.etree for better performance with large XML files. The dependency was already pulled in transitively.

Add comprehensive e2e tests to validate hostname display across all D2D
test reporting locations as specified in issue #482.

Changes:
- Add expected_d2d_hostnames field to E2EScenario dataclass
- Configure hostnames for D2D scenarios (SUCCESS, ALL_FAIL, MIXED,
  PYATS_D2D_ONLY, PYATS_CC)
- Add 6 hostname validation test methods to E2ECombinedTestBase:
  * Console output format: "Test Name (hostname) | STATUS |"
  * HTML summary tables and detail page headers
  * HTML filenames with sanitized hostnames
  * API test separation (no hostnames shown)
  * Character sanitization validation
- Add HTML parsing helpers for hostname extraction and validation
- Use exact sanitization logic matching base_test.py (r"[^a-zA-Z0-9]" → "_")
- Liberal pattern matching for resilience to format changes
- Smart skip logic for scenarios without D2D tests

Test coverage validates all aspects of hostname display implemented in PR #478:
- Console output displays hostnames in parentheses format
- HTML reports show hostnames in summary and detail views
- Filenames include properly sanitized hostnames
- API tests correctly exclude hostnames
- Cross-report consistency maintained

Resolves #482
Create a combined xunit.xml at the output root by merging results from
Robot Framework and PyATS test executions. This enables CI/CD pipelines
(Jenkins, GitLab) to consume a single test results file.

Changes:
- Add xunit_merger module using lxml.etree for efficient XML parsing
- Move Robot xunit.xml output to robot_results/ subdirectory
- Integrate merger into combined orchestrator post-execution
- Add lxml>=4.5.1 as direct dependency
- Add 17 unit tests for merger functionality
- Extend E2E tests to verify merged xunit.xml in all scenarios
@oboehmer oboehmer self-assigned this Feb 20, 2026
@oboehmer oboehmer added enhancement New feature or request new-infra Issues related to the new pyats/robot infra currently under development pyats PyATS framework related labels Feb 20, 2026
Use robot_results_dir Path object for xunit.xml path instead of
f-string with constant, consistent with how output paths will be
constructed in pabot 5.2 upgrade.
Copy link
Collaborator

@aitestino aitestino left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey Oliver, nice work on this one — the xunit merger is clean and the E2E test coverage is solid. A few things I noticed:

  1. collect_xunit_files() hardcodes "robot_results" and "pyats_results" as string literals, but we already have ROBOT_RESULTS_DIRNAME and PYATS_RESULTS_DIRNAME in nac_test/core/constants.py — and every other module in the codebase imports them from there. Would be good to stay consistent so if those paths ever change, this module doesn't silently break.

  2. In _extract_testsuite_stats(), the int() and float() calls on XML attributes will raise ValueError if a malformed xunit file has non-numeric values. The caller already catches ET.ParseError and OSError, but ValueError slips through. Adding it to that same exception handling block would keep the graceful-degradation behavior consistent.

  3. The merge_xunit_results() call in combined_orchestrator.py doesn't have a try/except around it — if something unexpected happens during the merge (permissions, disk full, etc.), it would crash the entire orchestrator run. A simple try/except with a warning log would make it resilient, since a failed merge shouldn't take down the test results.

  4. Minor one — the "--xunit" flag was added to both execute_job() and execute_job_with_testbed() in subprocess_runner.py. Those two methods already share a lot of duplicated command-building logic (pre-existing), but this adds one more thing to keep in sync. Not blocking, just worth noting for a future cleanup pass.

What do you think?

P.S. — This comment was drafted using voice-to-text via Claude Code. If the tone comes across as overly direct or terse, please know that's just how it tends to phrase things. No offense or criticism is intended — this is purely an objective technical review of the PR. Thanks for understanding! 🙂

Remove HostnameDisplayInfo dataclass and four extraction functions
(extract_hostname_from_display_text, extract_hostnames_from_summary_table,
extract_hostname_from_detail_page_header, extract_hostnames_from_filenames)
that were added during development but never used in the final tests.
Create sanitize_hostname() in nac_test/utils/strings.py using pattern
[^a-zA-Z0-9_] to clearly indicate underscores are safe characters.
Update base_test.py and job_generator.py to use the shared utility
instead of duplicating the regex pattern inline.
Add validation in E2EScenario.validate() ensuring expected_d2d_hostnames
is populated when has_pyats_d2d_tests > 0. This makes the redundant
"or not expected_d2d_hostnames" skip condition checks unnecessary in
the 5 hostname test methods, simplifying the test logic.
…strings

Replace hardcoded "robot_results" and "pyats_results" string literals
with ROBOT_RESULTS_DIRNAME and PYATS_RESULTS_DIRNAME constants from
nac_test/core/constants.py for consistency with the rest of the codebase.
Add ValueError to exception handling in merge_xunit_files() to gracefully
handle xunit files with non-numeric attribute values (e.g., tests="abc").
This maintains consistent graceful-degradation behavior alongside the
existing ParseError and OSError handling.
Wrap merge_xunit_results() call in try/except to prevent unexpected
errors (permissions, disk full, etc.) from crashing the entire test
run. A failed merge now logs a warning instead of terminating the
orchestrator, since test results are still valid without the merged file.
…mand helper

Consolidate duplicated PyATS command construction from execute_job() and
execute_job_with_testbed() into a single _build_command() method. This
reduces code duplication and ensures consistent command building including
--verbose/--quiet flag handling.

Related tech debt tracked in #570.
@oboehmer
Copy link
Collaborator Author

Thanks again, @aitestino, you were on a run on a Sunday 😊. Addressed all four points:

1. Constants for directory names (16080f5)
Good catch, sorry I missed those! 😊 Now using ROBOT_RESULTS_DIRNAME and PYATS_RESULTS_DIRNAME from nac_test/core/constants.py in collect_xunit_files().

2. ValueError handling in _extract_testsuite_stats() (711165e)
Added ValueError to the exception handling block alongside ET.ParseError and OSError.

3. try/except around merge_xunit_results() in orchestrator (20e09c7)
Wrapped the call with try/except and warning log — a merge failure now logs a warning instead of crashing the run.

4. Command-building duplication in subprocess_runner.py (5ea93b0)
Extracted a _build_command() helper method that both execute_job() and execute_job_with_testbed() now use. This consolidates the command construction including the --verbose/--quiet flag logic.

During the refactoring I also noticed some related tech debt around config file creation (duplicated code + an unused plugin_config_path attribute). I've opened #570 to track that separately since it's outside the scope of this PR.

Would you mind taking another look when you get a chance?

Copy link
Collaborator

@aitestino aitestino left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@oboehmer oboehmer merged commit a434700 into release/pyats-integration-v1.1-beta Feb 26, 2026
7 checks passed
@oboehmer oboehmer deleted the feature/xunit-merger branch February 26, 2026 07:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request new-infra Issues related to the new pyats/robot infra currently under development pyats PyATS framework related

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants