Skip to content

feat(pyats): add tag-based filtering for pyATS tests#657

Draft
oboehmer wants to merge 5 commits intomainfrom
feat/436-pyats-tag-filtering
Draft

feat(pyats): add tag-based filtering for pyATS tests#657
oboehmer wants to merge 5 commits intomainfrom
feat/436-pyats-tag-filtering

Conversation

@oboehmer
Copy link
Collaborator

@oboehmer oboehmer commented Mar 17, 2026

Description

This PR adds tag-based filtering support for PyATS tests using the --include and --exclude CLI options, bringing PyATS test selection in line with Robot Framework's tag filtering capabilities. PyATS tests are filtered based on their groups class attribute using Robot Framework's TagPatterns API for consistent pattern matching across both frameworks.

Closes

Related Issue(s)

  • N/A

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

nac-test supports macOS and Linux only

  • macOS (version tested: )
  • Linux (distro/version tested: )

Key Changes

Core Tag Filtering Implementation

  • New TagMatcher class (tag_matcher.py): Wraps Robot Framework's TagPatterns API to provide consistent tag matching behavior for both Robot and PyATS tests

    • Supports all Robot Framework tag pattern syntax: simple tags, wildcards (bgp*), boolean operators (healthANDbgp, bgpORospf, bgpNOTnrfu)
    • Case-insensitive matching with underscore normalization
    • Format helper methods for consistent error messaging
  • Refactored test discovery architecture (test_discovery.py, test_type_resolver.py):

    • Renamed TestTypeResolverTestMetadataResolver to reflect expanded responsibilities
    • AST-based extraction of groups attribute from PyATS test classes without importing
    • Pre-filtering during discovery phase eliminates subprocess overhead
    • Combined discovery and categorization into single pass returning PyatsDiscoveryResult
    • Removed caching (not needed with single-pass architecture)
  • New dataclasses (types.py):

    • TestFileMetadata: Structured representation of test file metadata (path, type, groups)
    • PyatsDiscoveryResult: Discovery results with pre-computed test_type_by_path mapping for O(1) lookups
  • Orchestrator integration (orchestrator.py):

    • Uses PyatsDiscoveryResult for efficient type lookups during result splitting (replaces re-instantiation of resolver)
    • Improved error messaging: "No pyATS tests matching tag filter (exclude: 'bgp OR ospf')" when all tests filtered out
    • Distinguishes between "no test files" vs "no tests matching filter"
  • Documentation (README.md, PRD_AND_ARCHITECTURE.md):

    • Updated --include/--exclude CLI option descriptions
    • Added PyATS groups attribute usage examples
    • Tag pattern syntax reference

Minor Cleanups

  • Remove unused ALL_SCENARIOS and get_scenario_by_name from e2e config
  • Remove unnecessary __future__ import from tag_matcher.py

Testing

New Tests for Tag Filtering

  • E2E tests (tests/e2e/):

    • 4 new scenarios covering include/exclude combinations
    • Fixtures with both Robot and PyATS tests tagged for filtering validation
    • Tests verify correct exit codes and test counts for various filter patterns
  • Unit tests (tests/unit/pyats_core/discovery/):

    • test_tag_matcher.py: Tag pattern matching, boolean logic, wildcards, edge cases
    • test_metadata_resolver.py: AST-based test type detection, directory fallback, error handling
    • test_groups_extraction.py: Groups attribute extraction for tag filtering
    • Updated orchestrator tests to use new PyatsDiscoveryResult structure

Test Suite Refactoring (Independent of Feature)

This PR includes test refactoring that improves the PyATS discovery test suite independently of the tag filtering feature:

Consolidation: Integration → Unit Tests

  • Moved 42 non-integration tests from test_discovery_integration.py to new tests/unit/pyats_core/discovery/test_test_discovery.py
  • Retained only TestDiscoveryPerformance as the true integration test (validates timing with 100+ files)
  • Result: Integration tests reduced from 43 → 1

Parametrization

  • Consolidated 39 individual test methods in test_tag_matcher.py into 15 parametrized tests (86 total cases)
  • Applied pytest parametrization per project testing guidelines

Code Quality

  • Added proper type annotations for all test parameters
  • Fixed mypy errors for generic types (listlist[str])
  • Added MockerFixture type hints for pytest-mock fixtures

Note: This refactoring improves test maintainability and follows project testing guidelines but is not strictly required for the tag filtering feature.

Testing Done

  • Unit tests added/updated
    • Unit tests in tests/unit/pyats_core/discovery/:
      • test_tag_matcher.py: Tag pattern matching with Robot Framework semantics
      • test_metadata_resolver.py: AST-based type detection and fallback logic
      • test_groups_extraction.py: Groups attribute extraction for filtering
    • Updated orchestrator tests to use new PyatsDiscoveryResult structure
  • E2E tests added
    • 4 tag filtering scenarios validating include/exclude combinations
  • 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

# Run all unit and integration tests
uv run pytest -n auto --dist loadscope

# Test tag filtering - include specific tag
nac-test -d ./tests/e2e/fixtures/tag_filtering -t ./tests/e2e/fixtures/tag_filtering/templates -o ./output --include bgp

# Test tag filtering - exclude specific tag
nac-test -d ./tests/e2e/fixtures/tag_filtering -t ./tests/e2e/fixtures/tag_filtering/templates -o ./output --exclude ospf

# Test tag filtering - boolean pattern
nac-test -d ./data -t ./tests -o ./output --include "healthORbgp" --exclude nrfu

# Test no matches (should exit with code 252)
nac-test -d ./tests/e2e/fixtures/tag_filtering -t ./tests/e2e/fixtures/tag_filtering/templates -o ./output --exclude "bgpORospf"

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

Implementation Approach

The tag filtering implementation leverages Robot Framework's TagPatterns API to ensure consistent behavior between Robot and PyATS test selection. Key design decisions:

  1. PyATS groups attribute: Uses the native pyATS mechanism (simple list of strings on test classes) for tagging

    class VerifyBgpNeighbors(SDWANTestBase):
        groups = ["bgp", "routing"]  # Matches --include bgp or --include routing
  2. Discovery-phase filtering: Tests are filtered during the discovery phase via AST parsing, avoiding subprocess overhead and enabling early validation

  3. Single-pass architecture: Discovery and categorization combined into one pass that returns a PyatsDiscoveryResult with pre-computed mappings for efficient O(1) lookups

  4. Consistent error messaging: Uses Robot Framework's TagPatterns string formatting for user-facing messages ('bgpORospf''bgp OR ospf')

Breaking Changes

None. The changes are backward-compatible:

  • PyATS tests without groups attribute remain unaffected and run as before
  • Existing --include/--exclude behavior for Robot Framework unchanged
  • All existing test repositories continue to work without modification

Migration Notes

  • PyATS tests can optionally add a groups class attribute to enable tag-based filtering
  • The groups attribute should be a list of strings (native pyATS format)
  • Tests without groups are treated as having no tags and are included by default (unless an --include pattern is specified)

@oboehmer oboehmer added enhancement New feature or request pyats PyATS framework related labels Mar 19, 2026
@oboehmer oboehmer force-pushed the feat/436-pyats-tag-filtering branch from 58b8ea2 to c944b81 Compare March 19, 2026 08:44
Extend --include and --exclude CLI options to filter pyATS tests based on
their `groups` class attribute using Robot Framework tag pattern semantics.

Changes:
- Add TagMatcher class wrapping Robot's TagPatterns API for consistent
  tag matching between Robot and pyATS tests
- Add TestMetadataResolver for AST-based extraction of test type and
  groups from pyATS test files without importing them
- Add TestFileMetadata and TestExecutionPlan dataclasses to types.py
  for structured test discovery results
- Integrate tag filtering into test discovery pipeline

Refactor test discovery architecture:
- Remove caching from TestMetadataResolver (previously TestTypeResolver)
- Combine discovery and categorization into single pass returning
  TestExecutionPlan with pre-computed test_type_by_path mapping
- Orchestrator now uses execution plan for O(1) type lookups during
  post-execution result splitting instead of re-instantiating resolver

Tag pattern syntax (from Robot Framework):
- Simple tags: 'health', 'bgp', 'ospf'
- Wildcards: 'bgp*', '?est'
- Boolean: 'healthANDbgp', 'healthORbgp', 'healthNOTnrfu'
- Case-insensitive, underscores ignored

Example usage:
  nac-test -d data/ -t tests/ -o out/ --include health --exclude nrfu

---

feat(pyats): improve "no tests found" message when filtered by tags

When all pyATS tests are filtered out by tag patterns, the previous
message "No PyATS test files (*.py) found" was misleading — files
existed but were excluded. Now displays a descriptive message like:

  No pyATS tests matching tag filter (exclude: 'bgp OR ospf')

---

test(e2e): add tag filtering e2e scenarios for Robot and pyATS

Add 4 e2e scenarios verifying --include/--exclude tag filtering
works correctly for both Robot and pyATS tests.

- TAG_FILTER_INCLUDE: --include bgp → 1 Robot + 1 PyATS pass
- TAG_FILTER_EXCLUDE: --exclude ospf → 1 Robot + 1 PyATS pass
- TAG_FILTER_COMBINED: --include api-only → 0 Robot (exit 252) + 1 PyATS pass
- TAG_FILTER_NO_MATCH: --exclude bgpORospf → 0 Robot + 0 PyATS (exit 252)

---

docs: update --include/--exclude documentation for pyATS tag filtering

---

fix(pyats): use absolute() instead of resolve() for symlink-safe paths

Replace Path.resolve() with Path.absolute() to preserve symlinks and
prevent ValueError when using relative_to() on paths that resolve
differently than their absolute form.
@oboehmer oboehmer force-pushed the feat/436-pyats-tag-filtering branch from c944b81 to 7ee38fb Compare March 19, 2026 08:47
Refactor discovery tests for better maintainability:
- Move test_test_type_resolver.py to tests/unit/pyats_core/discovery/
- Extract helpers.py with shared FIXTURES_DIR and create_mock_path
- Split groups extraction tests into separate test_groups_extraction.py
- Parametrize tests to reduce duplication (475→301 lines in main test file)
- Use _UNUSED_TEST_ROOT for tests using mock paths vs FIXTURES_DIR for real fixtures

Rename TestExecutionPlan to PyatsDiscoveryResult for clarity:
- Better reflects its purpose as discovery output, not execution context
- Update all references in discovery module and tests
…ports

- Add frozen=True and slots=True to PyatsDiscoveryResult for immutability
- Convert properties to cached_property for better performance
- Clarify skipped_files vs filtered_by_tags semantics in docstring
- Remove stale re-exports from discovery __init__.py and test_type_resolver
- Update test imports to use canonical locations (common.types)
- Rename execution_plan → discovery_result in orchestrator for consistency
…ization

Refactor the PyATS discovery test suite to eliminate redundant coverage,
consolidate overlapping tests, and apply pytest parametrization for
improved maintainability.

## Changes

### Phase 1: Consolidate integration tests into unit tests

- Move non-integration tests from test_discovery_integration.py to new
  tests/unit/pyats_core/discovery/test_test_discovery.py:
  - TestDiscoveryFiltering (5 parametrized tests)
  - TestRelaxedPathRequirements (4 parametrized tests)
  - TestExcludePaths (1 parametrized test)
  - TestErrorHandling (2 tests for unreadable file handling)

- Retain only TestDiscoveryPerformance in test_discovery_integration.py
  as it is the only true integration test (validates discovery timing
  with 100+ files)

### Phase 2: Parametrize test_tag_matcher.py

Consolidate 39 individual test methods into 15 parametrized tests:
- TestBasicMatching: Consolidate has_filters tests (4 → 1)
- TestIncludePatterns: Consolidate include tests (4 → 1, 13 cases)
- TestExcludePatterns: Consolidate exclude tests (4 → 1, 14 cases)
- TestCombinedPatterns: Consolidate combined tests (2 → 1, 8 cases)
- TestRobotPatternSemantics: Split into boolean + wildcard (7 → 2)
- TestEdgeCases: Consolidate edge case tests (9 → 4)
- TestFormatFilterDescription: Consolidate formatting tests (9 → 3)

### Code quality

- Add proper type annotations for all test parameters
- Fix mypy errors for generic types (list → list[str])
- Add MockerFixture type hints for pytest-mock fixtures

## Test Coverage Metrics

### Before refactor
| File                           | Tests | Coverage |
|--------------------------------|-------|----------|
| test_discovery_integration.py  | 43    | -        |
| test_tag_matcher.py            | 39    | 100%     |
| test_discovery.py (source)     | -     | 90%      |

### After refactor
| File                           | Tests | Coverage |
|--------------------------------|-------|----------|
| test_discovery_integration.py  | 1     | -        |
| test_test_discovery.py (new)   | 12    | -        |
| test_tag_matcher.py            | 15    | 100%     |
| test_discovery.py (source)     | -     | 95%      |
| tag_matcher.py (source)        | -     | 100%     |

### Summary
- Integration tests: 43 → 1 (moved 42 to unit tests)
- test_tag_matcher.py methods: 39 → 15 (86 parametrized cases)
- Total discovery test cases: 145 passed
- Overall discovery module coverage: 85%

## Validation

- All pre-commit hooks pass (ruff, mypy, license headers)
- All 145 tests pass
- No coverage regression
Add pytest configuration to eliminate collection warnings for source
classes in nac_test/ that start with "Test" prefix (e.g., TestDiscovery,
TestResults, TestFileMetadata).

Changes:
- Add nac_test to norecursedirs to prevent pytest from recursing into
  source directory during test collection
- Add filterwarnings with explicit regex matching the 10 Test* classes
  to suppress warnings when they are imported by test files

The regex explicitly names each class rather than using a wildcard to
avoid accidentally suppressing warnings for actual test issues.

Ref: #650
@oboehmer oboehmer self-assigned this Mar 19, 2026
@oboehmer oboehmer changed the base branch from release/pyats-integration-v1.1-beta to release/pyats-integration-v2.0 March 19, 2026 16:25
@oboehmer oboehmer changed the base branch from release/pyats-integration-v2.0 to release/pyats-integration-v1.1-beta March 20, 2026 21:56
@oboehmer oboehmer mentioned this pull request Mar 22, 2026
@oboehmer oboehmer changed the base branch from release/pyats-integration-v1.1-beta to main March 22, 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 pyats PyATS framework related

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant