Skip to content

feat(cli): add --verbose and --loglevel flags, align env vars to NAC_TEST_ prefix#599

Merged
oboehmer merged 57 commits intorelease/pyats-integration-v1.1-betafrom
feature/512-env-var-alignment-v2
Mar 9, 2026
Merged

feat(cli): add --verbose and --loglevel flags, align env vars to NAC_TEST_ prefix#599
oboehmer merged 57 commits intorelease/pyats-integration-v1.1-betafrom
feature/512-env-var-alignment-v2

Conversation

@oboehmer
Copy link
Collaborator

@oboehmer oboehmer commented Feb 28, 2026

Description

Standardize internal environment variables to use NAC_TEST_PYATS_ prefix for consistency with the public NAC_TEST_ convention. Add --verbose CLI flag for easier troubleshooting. Implement --loglevel flag support to control log detail levels for both PyATS and Robot Framework.

Environment variable renames:

Old Name New Name
PYATS_MAX_WORKERS NAC_TEST_PYATS_PROCESSES
MAX_CONNECTIONS NAC_TEST_PYATS_MAX_CONNECTIONS
NAC_API_CONCURRENCY NAC_TEST_PYATS_API_CONCURRENCY
NAC_SSH_CONCURRENCY NAC_TEST_PYATS_SSH_CONCURRENCY
PYATS_OUTPUT_BUFFER_LIMIT NAC_TEST_PYATS_OUTPUT_BUFFER_LIMIT
KEEP_HTML_REPORT_DATA NAC_TEST_PYATS_KEEP_REPORT_DATA
NAC_TEST_OVERFLOW_DIR NAC_TEST_PYATS_OVERFLOW_DIR
PYATS_DEBUG NAC_TEST_DEBUG

New --verbose flag behavior:

  • Enables verbose output for pabot (--verbose) showing Robot console output
  • Enables PyATS console output (without --verbose, PyATS output is suppressed)
  • Implies --loglevel DEBUG unless explicitly overridden

--loglevel flag controls log detail level:

--loglevel PyATS console output PyATS internal loglevel Robot --loglevel
WARNING (default) Suppressed (unless --verbose) WARNING Not set (Robot default)
ERROR Suppressed (unless --verbose) ERROR Not set
INFO Suppressed (unless --verbose) INFO Not set
DEBUG Suppressed (unless --verbose) DEBUG DEBUG

Key behaviors:

  • PyATS console output requires --verbose. Without it, PyATS output is suppressed regardless of --loglevel.
  • --verbose: Enables console output for both frameworks, implies --loglevel DEBUG
  • --verbose --loglevel INFO: Shows PyATS console output filtered to INFO+, Robot uses default loglevel
  • --verbose --loglevel ERROR: Shows PyATS console output filtered to ERROR+ only, Robot uses default loglevel
  • --loglevel controls nac-test internal logging level in addition to framework output (independent of --verbose), no change in this PR

Deprecation notice:

  • --verbosity is now a hidden alias for --loglevel and will be removed in a future version. Use --loglevel instead.

Breaking change:

  • Robot Framework --loglevel can no longer be controlled via pass-through arguments. See README for workarounds using --variable and Set Log Level keyword.

PyATS output filtering is optimized for high-volume runs (benchmarked: 412k lines in <0.1s).

Note: To preserve intermediate archive/JSONL files for troubleshooting, set NAC_TEST_DEBUG=true or NAC_TEST_PYATS_KEEP_REPORT_DATA=true as environment variables. The --verbose CLI flag enables verbose output but does not retain these files.

The following env vars remain as undocumented internal tuning knobs, however I have moved all of them to nac_test/pyats_core/constants.py so we can manage them centrally. Due to this move I changed the respective internal constant name:

Environment Variable Old Constant Name New Constant Name
NAC_TEST_PYATS_PIPE_DRAIN_DELAY PIPE_DRAIN_DELAY_SECONDS (inline parsing) PIPE_DRAIN_DELAY_SECONDS
NAC_TEST_PYATS_PIPE_DRAIN_TIMEOUT PIPE_DRAIN_TIMEOUT_SECONDS (inline parsing) PIPE_DRAIN_TIMEOUT_SECONDS
NAC_TEST_PYATS_BATCH_SIZE (inline in batching_reporter.py) BATCH_SIZE
NAC_TEST_PYATS_BATCH_TIMEOUT (inline in batching_reporter.py) BATCH_TIMEOUT_SECONDS
NAC_TEST_PYATS_QUEUE_SIZE (inline in batching_reporter.py) OVERFLOW_QUEUE_SIZE
NAC_TEST_PYATS_MEMORY_LIMIT_MB (inline in batching_reporter.py) OVERFLOW_MEMORY_LIMIT_MB

Note: SENTINEL_TIMEOUT_SECONDS / NAC_TEST_PYATS_SENTINEL_TIMEOUT were introduced in d1c3865 but never wired into the subprocess runner. Removed entirely in commit 3ab23b3 within this PR — see that commit message for the full reasoning.

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: 15.3)
  • Linux (distro/version tested: )

Key Changes

  • Add --verbose CLI flag with env var support (NAC_TEST_VERBOSE)
  • --verbose enables verbose output for both pabot and PyATS execution
  • --verbose implies DEBUG loglevel unless --loglevel is explicitly set
  • Add --loglevel CLI argument to control log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
  • Deprecate --verbosity argument (hidden alias for --loglevel, will be removed in future version)
  • PyATS: Console output only shown when --verbose is set; --loglevel filters what is shown
  • PyATS: Respect --loglevel in generated job files via managed_handlers.screen.setLevel()
  • Robot: Decouple verbose (pabot console) from loglevel (Robot --loglevel)
  • Robot: Set --loglevel DEBUG only when nac-test --loglevel DEBUG
  • BREAKING: Robot Framework --loglevel pass-through no longer supported; use --variable approach documented in README
  • Add LogLevel enum class with comparison operators
  • Update README with Verbose Mode and Advanced Environment Variables documentation
  • Add unit tests for verbose/loglevel interaction, job generation, loglevel precedence
  • Add E2E test scenarios to verify --verbose and --verbose --loglevel INFO behavior

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 pytest -n auto --dist loadscope  # Full test suite
uv run pytest tests/unit/cli/ -v
uv run pytest tests/unit/robot/ -v
uv run pytest tests/unit/pyats_core/execution/ -v
uv run pytest tests/unit/utils/test_logging.py -v
uv run pytest tests/unit/test_pabot_args.py -v
uv run pytest tests/e2e -k verbose -v
uv run pre-commit run --all-files

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)

…debug flag (#512)

Standardize internal environment variables to use NAC_TEST_PYATS_ prefix
for consistency with the public NAC_TEST_ convention. Add --debug CLI flag
for easier troubleshooting with verbose output and archive retention.

Environment variable renames:
- PYATS_MAX_WORKERS → NAC_TEST_PYATS_PROCESSES
- MAX_CONNECTIONS → NAC_TEST_PYATS_MAX_CONNECTIONS
- NAC_API_CONCURRENCY → NAC_TEST_PYATS_API_CONCURRENCY
- NAC_SSH_CONCURRENCY → NAC_TEST_PYATS_SSH_CONCURRENCY
- PYATS_OUTPUT_BUFFER_LIMIT → NAC_TEST_PYATS_OUTPUT_BUFFER_LIMIT
- KEEP_HTML_REPORT_DATA → NAC_TEST_PYATS_KEEP_REPORT_DATA
- NAC_TEST_OVERFLOW_DIR → NAC_TEST_PYATS_OVERFLOW_DIR
- PYATS_DEBUG → NAC_TEST_DEBUG

New --debug flag enables verbose output for pabot/PyATS and preserves
intermediate archive files for troubleshooting.

The following env vars remain as undocumented internal tuning knobs,
not exposed as CLI flags:
- NAC_TEST_SENTINEL_TIMEOUT
- NAC_TEST_PIPE_DRAIN_DELAY
- NAC_TEST_PIPE_DRAIN_TIMEOUT
- NAC_TEST_BATCH_SIZE
- NAC_TEST_BATCH_TIMEOUT
- NAC_TEST_QUEUE_SIZE
- Replace obsolete NAC_API_CONCURRENCY, NAC_SSH_CONCURRENCY with new
  NAC_TEST_PYATS_* prefixed variables
- Add all documented PyATS tuning variables to diagnostic output
- Add separate section for undocumented internal tuning variables
…idden

When --debug is set without an explicit --verbosity flag, verbosity now
defaults to DEBUG instead of WARNING. Explicit --verbosity always wins.

- Add tests for all debug/verbosity flag combinations (11 test cases)
- Extract shared run_cli_with_temp_dirs() helper to conftest.py
- Refactor test_main_exit_codes.py to use shared helper
Replace 8 repetitive test methods with a single parametrized test,
reducing boilerplate while maintaining the same coverage.
@oboehmer oboehmer linked an issue Feb 28, 2026 that may be closed by this pull request
Wire --verbosity flag through to OutputProcessor so PyATS log messages
are filtered consistently with nac-test's own logger output. This gives
users control over PyATS noise without requiring the NAC_TEST_DEBUG
environment variable.

Behavior by verbosity level:
- WARNING (default) or ERROR: Suppress all PyATS logs, show only critical
  messages (ERROR, FAILED, Traceback, etc.)
- INFO (-v INFO): Show PyATS logs at INFO level and above
- DEBUG (-v DEBUG or --debug): Show all output

Performance optimizations:
- Pre-compile regex patterns at module load time
- Early exit paths based on verbosity level
- Fast string check before regex for common patterns
- Benchmarked against 412k lines: <0.1s at default verbosity

Changes:
- Add debug and verbosity parameters to OutputProcessor
- Thread verbosity through PyATSOrchestrator and CombinedOrchestrator
- Replace DEBUG_MODE env check with self.debug for section progress
- Convert parse failure print() to logger.debug()
- Update e2e test: --debug now shows PyATS output (implies DEBUG verbosity)
The TestE2EDebugEnv test was redundant with TestE2EDebug since both
test the same debug behavior. The env var test only verified that
Typer's envvar= parameter works, which is Typer's responsibility.

The actual debug behavior (verbose output, PyATS logs) is already
covered by TestE2EDebug which uses the --debug CLI flag.

Note: The extra_env_vars parameter in _run_e2e_scenario is retained
for potential future use cases.
@oboehmer oboehmer force-pushed the feature/512-env-var-alignment-v2 branch from b8dcb9e to ebec315 Compare February 28, 2026 19:52
oboehmer added 17 commits March 1, 2026 08:10
Fix issue where PyATS ignores nac-test's --verbosity flag. The root cause
was that aetest's configure_logging() overwrites standard logging config.

Solution: Set managed_handlers.screen.setLevel() directly in generated job
files, which is the only reliable way to control PyATS console output.

Changes:
- JobGenerator: Accept verbosity param, set screen handler level in job files
- SubprocessRunner: Map verbosity to PyATS CLI flags (-v, -q, -qq, -qqq)
- OutputProcessor: Simplify _should_show_line() since filtering now happens
  at source via managed_handlers
- logging.py: Add VERBOSITY_TO_LOGLEVEL and LOGLEVEL_NAME_TO_VALUE mappings
  as module-level constants for reuse across components

Tests:
- Add unit tests for job file generation (test_job_generator.py)
- Add unit tests for verbosity CLI flag construction
- Add unit tests for VERBOSITY_TO_LOGLEVEL mapping
- Add E2E scenario (DEBUG_WITH_INFO_SCENARIO) verifying --debug --verbosity INFO
  filters %EASYPY-DEBUG while showing %EASYPY-INFO

Docs:
- Update README with --verbosity interaction with --debug flag
Decouple Robot's verbose output (pabot --verbose) from loglevel
(--loglevel). Previously both were tied to --debug flag.

Now:
- --debug: enables pabot verbose output (console)
- --verbosity DEBUG: sets Robot --loglevel DEBUG
- --verbosity INFO/WARNING/etc: no --loglevel set (Robot default)
- Explicit --loglevel arg always takes precedence

This aligns Robot behavior with PyATS verbosity handling.

Changes:
- Add loglevel parameter to run_pabot(), separate from verbose
- Update RobotOrchestrator to compute loglevel from verbosity
- Add unit tests for loglevel precedence logic
- Update README to document the behavior
- Reduce test permutations from 11 to 7 (remove -v alias and order tests)
- Add verification that verbosity and debug are passed to CombinedOrchestrator
- Add descriptive test IDs for better readability
…ired

- Make verbosity a required parameter in JobGenerator.__init__ to ensure
  explicit intent and prevent test escapes if verbosity handling is removed
- Refactor test_job_generator.py to use base class pattern with fixtures:
  - Add default_test_files fixture for tests that don't care about paths
  - Use generate_content fixture consistently across all tests
  - Make verbosity optional in generate_content callable (defaults to WARNING)
- Remove redundant test_contains_screen_handler_setlevel (covered by
  parametrized test_verbosity_mapped_to_screen_handler)
- Remove test_init_default_verbosity (no longer applicable with required param)
- Reverse parameter order in generate_content: test_files first, then verbosity
Add DEFAULT_VERBOSITY constant to nac_test/utils/logging.py to centralize
the default verbosity level (WARNING). This makes it easier to change the
default in one place if needed in the future.

Changes:
- Add DEFAULT_VERBOSITY = VerbosityLevel.WARNING in utils/logging.py
- Update 6 source files to import DEFAULT_VERBOSITY from utils.logging
- Update 3 test files to use DEFAULT_VERBOSITY for default behavior tests

Tests that check specific verbosity level behavior (parametrized tests
covering all levels, explicit --verbosity flag tests) intentionally
remain hardcoded to test each level individually.
Add _get_positive_numeric() helper to core/constants.py that validates
environment variables return positive values, falling back to defaults
otherwise. This consolidates scattered inline parsing patterns.

Changes:
- core/constants.py: Add TypeVar-based helper for int/float support,
  update DEFAULT_API_CONCURRENCY and DEFAULT_SSH_CONCURRENCY to use it
- pyats_core/constants.py: Import helper and migrate all env var
  constants (buffer limit, sentinel timeout, pipe drain, batching)
- Rename DEFAULT_BUFFER_LIMIT → PYATS_OUTPUT_BUFFER_LIMIT
- Move BATCH_SIZE, BATCH_TIMEOUT_SECONDS, OVERFLOW_QUEUE_SIZE,
  OVERFLOW_MEMORY_LIMIT_MB from batching_reporter.py to constants.py

Helper is defined in core/ (not utils/) to avoid circular import chain:
core/constants → utils/ → utils/environment → core/constants
Update PRD_AND_ARCHITECTURE.md Progress Reporting section to include
explicit method references for each component, making it easier for
developers to navigate the codebase:

- ProgressReporterPlugin: _emit_event(), pre/post_job(), pre/post_task()
- OutputProcessor: process_line(), _handle_progress_event(), _finalize_orphaned_tests()
- ProgressReporter: report_test_start(), report_test_end(), get_next_test_id()
- SubprocessRunner: execute_job(), _process_output_realtime(), _drain_remaining_buffer_safe()

Keeps descriptions high-level without exposing internal implementation
details, while providing clear entry points for code exploration.
The --debug flag name was misleading as it controls verbose output rather
than debug-level functionality. Rename to --verbose with corresponding
NAC_TEST_VERBOSE environment variable.

Changes:
- CLI: --debug → --verbose flag
- Env var: NAC_TEST_DEBUG → NAC_TEST_VERBOSE (user-facing)
- Internal: debug parameter → verbose in orchestrators
- Tests: rename fixtures/debug → fixtures/verbose, update test classes
- Docs: update README.md and PRD_AND_ARCHITECTURE.md

Note: NAC_TEST_DEBUG is retained as a hidden variable for developer-level
debugging (DEBUG_MODE in constants.py).
@oboehmer oboehmer changed the title feat(env): align environment variables to NAC_TEST_ prefix and add --debug flag feat(cli): add --verbose and --loglevel flags, align env vars to NAC_TEST_ prefix Mar 2, 2026
Rename the CLI argument from --verbosity to --loglevel for clarity, as it
controls the logging level, not verbosity. The --verbose flag now controls
verbose output mode separately.

Key changes:

- Rename VerbosityLevel enum to LogLevel with enhanced functionality
  (int_value property, comparison operators)
- Add --loglevel (-l) as the new CLI argument for log level control
- Deprecate --verbosity (-v) as hidden alias for backwards compatibility
- Add --verbose flag for enabling verbose output mode
- Simplify Robot loglevel: only set to DEBUG when nac-test loglevel is DEBUG
- Rename run_pabot's loglevel parameter to robot_loglevel
- Update all internal variable names from verbosity to loglevel
- Remove unused NAC_VALIDATE_VERBOSITY env var (use NAC_TEST_LOGLEVEL)
- Update README with new CLI options and breaking change documentation

BREAKING CHANGE: Robot Framework --loglevel pass-through no longer supported.
Use --variable approach documented in README instead.
oboehmer and others added 11 commits March 4, 2026 10:01
DEBUG is the lowest log level, so <= is misleading and suggests
impossible states. Aligns with other DEBUG checks in the codebase.
The orchestrator and connection manager were using non-standard env var names
(PYATS_MAX_WORKERS, MAX_SSH_CONNECTIONS) that didn't match the documented
NAC_TEST_PYATS_* naming convention. The existing unit tests only tested the
SystemResourceCalculator utility with default parameters, missing the fact
that callers were overriding the env_var parameter with incorrect names.

Add integration tests that verify the actual call sites use the correct
env var names, preventing future regressions.

- nac_test/pyats_core/orchestrator.py: PYATS_MAX_WORKERS -> NAC_TEST_PYATS_PROCESSES
- nac_test/pyats_core/ssh/connection_manager.py: MAX_SSH_CONNECTIONS -> NAC_TEST_PYATS_MAX_SSH_CONNECTIONS
- dev-docs/: Update stale env var references in documentation
SENTINEL_TIMEOUT_SECONDS (env: NAC_TEST_PYATS_SENTINEL_TIMEOUT) was
introduced in d1c3865 as a deadlock guard for sentinel-based IPC
synchronization, but was never wired into the subprocess runner.

The sentinel mechanism in _process_output_realtime() works by reading
lines until EOF, then falling back to _drain_remaining_buffer_safe()
if no stream_complete sentinel was received. A meaningful timeout would
require wrapping the entire post-EOF drain wait — which is already
covered by PIPE_DRAIN_TIMEOUT_SECONDS. There is no natural place to
apply a separate sentinel timeout without a design change.

Remove the constant definition, __all__ export, diagnostic script entry,
and PRD_AND_ARCHITECTURE.md table row. The env var name is left available
for a future PR if a deadlock guard is ever implemented.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…checks

Replace raw os.environ.get("NAC_TEST_DEBUG") checks in orchestrator.py
and multi_archive_generator.py with the DEBUG_MODE constant from
core/constants.py. This ensures consistent semantics (only "true"
triggers debug mode, not empty strings or other truthy values) and
uses the single source of truth for the flag.

Also update log messages to use "or" instead of "/" for clarity.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…t.py

Replace per-fixture local imports of scenario constants with a single
module-level import block. All scenario constants from tests.e2e.config
are now imported at the top of the file, consistent with standard Python
import conventions and the existing module-level E2EScenario import.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add a comment explaining why pyats.log.managed_handlers is imported
inside the test body rather than at module level: a collection-time
ImportError gives too little context; keeping it here ensures failures
surface in the right test with a clear name.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add a comment explaining why test_pabot_verbose_shows_test_result and
test_easypy_info_in_stdout are duplicated from TestE2EVerbose rather
than extracted into an intermediate base class.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The helper is intentionally imported across package boundaries (to avoid
circular imports), so a leading underscore is misleading. The new name
signals it is a shared utility and clarifies its purpose.

Add unit tests in tests/unit/core/test_constants.py covering int and
float parsing, zero/negative/non-numeric/empty fallback to default,
and the not-set case.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the hardcoded /tmp/test.py with a tmp_path-based file,
making the fixture portable and consistent with pytest conventions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
apic.test.com could theoretically resolve; replace with RFC 2606
.invalid to make the non-functional intent explicit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@aitestino
Copy link
Collaborator

aitestino commented Mar 5, 2026

Hey @oboehmer, thank you for the PR — the motivation here is solid. Standardizing the env var naming to the NAC_TEST_* prefix, adding --verbose, and introducing --loglevel are all moves in the right direction for consistency and usability. THANK YOU!!!

I did a comprehensive review on this, and I think we need some changes before merge. The implementation introduces a few patterns that don't align with our existing codebase conventions, and more critically, there are some runtime-breaking issues and incomplete refactoring steps that would cause the application to fail on startup.

TLDR-ish:

1. Runtime TypeError: configure_logging() signature mismatch

The diff shows configure_logging(level: str | LogLevel) -> None (removing the error_handler parameter), but the current utils/logging.py on disk still has the 2-parameter signature with error_handler: errorhandler.ErrorHandler. The diff also doesn't show removal of the error_handler.reset() call in the function body (line 57-58 of current file).

Meanwhile, cli/main.py in the diff calls it with just one argument: configure_logging(effective_loglevel).

If we apply this diff as-is, we'll get either a TypeError (wrong number of arguments) or NameError: name 'error_handler' is not defined at runtime.

What I'm wondering: Was the error_handler integration intentionally removed? If so, we need to:

  • Remove the error_handler.reset() call from the function body
  • Remove the import errorhandler from logging.py
  • Remove the error_handler = ErrorHandler() instantiation from main.py (line 377 currently checks error_handler.fired)

Or if we still need it, restore the parameter to the signature.


2. Runtime TypeError: CombinedOrchestrator doesn't accept loglevel= and verbose= kwargs

The diff adds to cli/main.py:

orchestrator = CombinedOrchestrator(
    ...
    loglevel=effective_loglevel,  # NEW
    verbose=verbose,              # NEW
)

But the current CombinedOrchestrator.__init__() signature accepts verbosity: VerbosityLevel, not loglevel or verbose.

This will raise TypeError: __init__() got unexpected keyword argument 'loglevel' on startup.

Options:

  • Update CombinedOrchestrator.__init__ to accept loglevel and verbose, OR
  • Pass verbosity=effective_loglevel instead (though we'd need to ensure type compatibility since effective_loglevel is LogLevel and the signature expects VerbosityLevel)

3. Six new constants defined but never wired up to their consumers (dead code)

The PR adds these constants to pyats_core/constants.py:

PYATS_OUTPUT_BUFFER_LIMIT = get_positive_numeric_env("NAC_TEST_PYATS_OUTPUT_BUFFER_LIMIT", ...)
BATCH_SIZE = get_positive_numeric_env("NAC_TEST_PYATS_BATCH_SIZE", ...)
BATCH_TIMEOUT_SECONDS = get_positive_numeric_env("NAC_TEST_PYATS_BATCH_TIMEOUT", ...)
OVERFLOW_QUEUE_SIZE = get_positive_numeric_env("NAC_TEST_PYATS_QUEUE_SIZE", ...)
OVERFLOW_MEMORY_LIMIT_MB = get_positive_numeric_env("NAC_TEST_PYATS_MEMORY_LIMIT_MB", ...)
OVERFLOW_DIR_OVERRIDE = os.environ.get("NAC_TEST_PYATS_OVERFLOW_DIR")

And exports them in __all__.

But: The actual consumers aren't updated to import/use them:

  • subprocess_runner.py still imports DEFAULT_BUFFER_LIMIT and reads os.environ.get("PYATS_OUTPUT_BUFFER_LIMIT") (the old env var name)
  • batching_reporter.py still reads os.environ.get("NAC_TEST_BATCH_SIZE") directly (note: different env var name than what the constant reads — NAC_TEST_BATCH_SIZE vs NAC_TEST_PYATS_BATCH_SIZE)

Impact: If a user sets NAC_TEST_PYATS_OUTPUT_BUFFER_LIMIT or NAC_TEST_PYATS_BATCH_SIZE, those values are silently ignored because nothing actually consumes these constants.

What do you think? We could either:

  1. Wire up all 6 constants to their consumers in this PR (update imports in subprocess_runner.py and batching_reporter.py), OR
  2. Remove them from this PR and add them when we're ready to actually integrate them

Also, we need to align on env var naming: should it be NAC_TEST_BATCH_SIZE or NAC_TEST_PYATS_BATCH_SIZE?


4. NO_TESTLEVELSPLIT constant has wrong semantics and is never imported

The PR adds:

# core/constants.py
NO_TESTLEVELSPLIT = bool(os.environ.get("NAC_TEST_NO_TESTLEVELSPLIT"))

But robot/orchestrator.py still uses:

if "NAC_TEST_NO_TESTLEVELSPLIT" not in os.environ:

These have different semantics:

# User sets empty string
export NAC_TEST_NO_TESTLEVELSPLIT=""

# Old code: "VAR" in os.environ → True (split disabled)
# New constant: bool(os.environ.get("VAR")) → False (split enabled!)

Since the constant is never actually imported by robot/orchestrator.py, it's dead code right now. But if we wire it up later, behavior will silently change for the empty-string case.

What I'm suggesting:

  • Use "NAC_TEST_NO_TESTLEVELSPLIT" in os.environ for semantic equivalence with the current behavior, OR
  • Document that empty string now means "enabled" (breaking change), OR
  • Just delete the constant if we're not ready to wire it up yet

Also: align with the DEBUG_MODE pattern in the same file, which uses .lower() in ("true", "1", "yes") instead of bool().


5. Incomplete VerbosityLevelLogLevel rename causing ImportError

The rename is applied to:

  • nac_test/utils/logging.py (class definition)
  • nac_test/cli/main.py (import and usage)
  • nac_test/utils/__init__.py (export)

But NOT applied to:

  • nac_test/robot/orchestrator.py — still imports VerbosityLevel
  • nac_test/combined_orchestrator.py — still imports VerbosityLevel
  • tests/unit/robot/test_orchestrator.py — still imports VerbosityLevel

Result: ImportError: cannot import name 'VerbosityLevel' from 'nac_test.utils.logging'

What I'm suggesting (pick one):

Option A (Recommended): Add a backward compatibility alias:

# nac_test/utils/logging.py
class LogLevel(str, Enum):
    # ... implementation

# Backward compatibility - DEPRECATED, remove in v3.0.0
VerbosityLevel = LogLevel

Option B: Expand this PR to complete the global rename across all files

Option C: Split into 2 PRs: first add the alias, then migrate all imports

From a best practices standpoint, I think Option A is safest — it gives us backward compatibility during the transition period and we can remove the alias in a future major version.


6. Missing test coverage for new LogLevel comparison operators

The PR adds __le__, __lt__, __ge__, __gt__ methods to LogLevel, but there are no tests verifying:

  • Comparison correctness: LogLevel.DEBUG < LogLevel.INFO
  • Integration with Python logging: int(LogLevel.DEBUG) == logging.DEBUG
  • Edge cases: LogLevel.DEBUG <= LogLevel.DEBUG

What I'm suggesting: Add a new test file tests/unit/utils/test_logging.py:

def test_loglevel_ordering():
    assert LogLevel.DEBUG < LogLevel.INFO < LogLevel.WARNING < LogLevel.ERROR < LogLevel.CRITICAL

def test_loglevel_int_conversion():
    assert int(LogLevel.DEBUG) == 10
    assert int(LogLevel.INFO) == 20

7. get_positive_numeric_env() silently ignores invalid input

When a user sets an invalid env var value:

export NAC_TEST_PYATS_API_CONCURRENCY=abc

The helper silently returns the default (55) with no warning or feedback.

What I'm suggesting: Add optional warning logging:

def get_positive_numeric_env(
    env_var: str, default: T, value_type: type[T], warn_on_invalid: bool = True
) -> T:
    env_value = os.environ.get(env_var)
    if env_value is None:
        return default
    
    try:
        value = value_type(env_value)
        if value > 0:
            return value
        if warn_on_invalid:
            logger.warning(f"{env_var}={env_value} is not positive, using default {default}")
    except (ValueError, TypeError):
        if warn_on_invalid:
            logger.warning(f"{env_var}={env_value} is not a valid {value_type.__name__}, using default {default}")
    return default

This follows the "fail visibly" principle — users should know when their configuration is being ignored.


8. get_positive_numeric_env() placement in constants.py violates SRP

The function is placed in core/constants.py, and the comment even acknowledges this: "lives here (not in utils/) to avoid circular imports".

Circular imports are an architectural symptom, not a justification for putting functions in constants files. constants.py should only contain constants per our conventions.

What I'm suggesting: Create nac_test/utils/env.py and move the function there. If there's a circular import issue, let's solve it properly rather than work around it by polluting the constants module.


9. Type annotation coverage: 26 constants missing annotations

Looking at core/constants.py and pyats_core/constants.py, there are 26 constants without type annotations. I thiiiink general project conventions we have been following dictate something like: "Named Constants: Type-annotated, SCREAMING_SNAKE_CASE."

For example:

# Current (unannotated):
RETRY_MAX_ATTEMPTS = 3
DEFAULT_TEST_TIMEOUT = 21600

# Required:
RETRY_MAX_ATTEMPTS: int = 3
DEFAULT_TEST_TIMEOUT: int = 21600

Only IS_MACOS: bool and IS_UNSUPPORTED_MACOS_PYTHON: bool have annotations currently.

I realize these might be pre-existing, but since we're touching both constants files in this PR, would be good to bring them into compliance.


10. Consider @functools.total_ordering to reduce type: ignore comments

LogLevel has four comparison methods with four # type: ignore[override] comments. We can reduce this to just one by using @functools.total_ordering:

from functools import total_ordering

@total_ordering
class LogLevel(str, Enum):
    def __lt__(self, other: "LogLevel") -> bool:  # type: ignore[override]
        return int(self) < int(other)
    # __le__, __gt__, __ge__ auto-generated by decorator

This reduces mypy blind spots from 4 locations to 1.


11. Missing module docstring and function docstrings

  • cli/main.py is missing a module-level docstring (all other files in this batch have one)
  • version_callback() is missing a docstring

Both are required per our conventions.


12. Minor: NO_TESTLEVELSPLIT boolean parsing inconsistency

NO_TESTLEVELSPLIT uses bool(os.environ.get(...)) while DEBUG_MODE in the same file uses .lower() == "true".

The bool() approach has surprising behavior — any non-empty string is truthy, including "0" and "false".

For consistency: Use the pattern from DEBUG_MODE:

NO_TESTLEVELSPLIT = os.environ.get("NAC_TEST_NO_TESTLEVELSPLIT", "").lower() in ("true", "1", "yes")

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!

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.

See above.

oboehmer added 5 commits March 5, 2026 07:02
Address PR 599 review comments 7 and 8:

- 7: Add warning logging when env var contains invalid (non-numeric) or
  non-positive values. Silent fallback to default only when env var is unset.
  New warn_on_invalid parameter allows callers to suppress warnings if needed.

- 8: Move get_positive_numeric_env() from core/constants.py to new
  nac_test/_env.py module. The underscore prefix signals internal API.
  Placement at package root avoids circular import via utils/__init__.py
  (see #610 for broader discussion on utils re-export strategy).

Tests added for warning behavior (4 new test cases).
Address PR review comment 9:

- core/constants.py: Add annotations to 25 public constants
- pyats_core/constants.py: Add annotations to 10 public constants

Private variables (e.g., _pipe_drain_default) left unannotated.
Address PR review comment 11:

- Add module docstring
- Add docstring to version_callback()
…TLEVELSPLIT

PR review comment 12: Add get_bool_env() helper for consistent boolean env var
parsing (accepts 'true', 'yes', '1'). Rename NO_TESTLEVELSPLIT to
DISABLE_TESTLEVELSPLIT for clarity and NAC_TEST_ prefix alignment.

Breaking change: NAC_TEST_NO_TESTLEVELSPLIT is now NAC_TEST_DISABLE_TESTLEVELSPLIT
Move NAC_TEST_BATCHING_REPORTER env var parsing to centralized constant
using get_bool_env(). Update step_interceptor.py to use the constant.
@oboehmer
Copy link
Collaborator Author

oboehmer commented Mar 5, 2026

Thanks for the review, @aitestino ! I've addressed comments 7-12:

Comment 7 - Warning logging for invalid env vars: Added warning logs to get_positive_numeric_env() when env var is set but invalid/non-positive. Controlled via warn_on_invalid parameter (default: True) 023482e

Comment 8 - Relocate helper: Moved get_positive_numeric_env() to nac_test/_env.py at package root. Placed there instead of utils/ to avoid circular import issues with utils/__init__.py re-exports. I also raised #610 to review the __all__ re-export strategy as they aren't consistently applied across the codebase (023482e)

Comment 9 - Type annotations: Added explicit type annotations to all public constants in core/constants.py and pyats_core/constants.py (139cf97)

Comment 10 - @functools.total_ordering: Investigated this - unfortunately it doesn't work with (str, Enum) inheritance. The decorator adds generated comparison methods, but str already defines __gt__, __ge__, etc. and takes MRO precedence. With @total_ordering applied, LogLevel.CRITICAL > LogLevel.ERROR returns False (alphabetic comparison) instead of using our severity-based ordering. The explicit overrides with # type: ignore[override] seem to be required.

Comment 11 - Docstrings: Added module docstring and version_callback() docstring to cli/main.py (50d68ab)

Comment 12 - get_bool_env() and constant rename: Added get_bool_env() helper for consistent boolean parsing (accepts true/yes/1). Renamed NO_TESTLEVELSPLITDISABLE_TESTLEVELSPLIT for clarity. (4f80bce) Also added BATCHING_REPORTER_ENABLED constant to pyats_core/constants.py (64926a9)


Regarding comments 1-6: Not sure what state you were reviewing, but none of these issues are present on the current branch. For example, configure_logging() has only the single level parameter, CombinedOrchestrator.__init__() already accepts loglevel= and verbose=, and VerbosityLevel has been fully renamed to LogLevel across all files including robot/orchestrator.py and combined_orchestrator.py.

I would appreciate a quick re-check so we can unblock the alpha release.

@oboehmer oboehmer requested a review from aitestino March 5, 2026 08:23
oboehmer added 2 commits March 5, 2026 10:10
The pabot.writer module uses a singleton that captures stdout at creation
time. When running multiple E2E scenarios sequentially, the second test
would reuse the same singleton with a stale stdout reference, causing
verbose output to go to the wrong stream.

This fix resets the singleton at the start of each scenario execution.

Workaround for #611
@oboehmer oboehmer force-pushed the feature/512-env-var-alignment-v2 branch from 1297224 to 96eb8ad Compare March 5, 2026 09:54
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 @oboehmer, thank you for the detailed response and for the patience on this.

You're right about comments 1-6 — I went back and verified each one against e960c6c, and the code was already in the correct state when I reviewed. It looks like my review was comparing against a stale base rather than looking at the actual branch files. That's my mistake, and I apologize for the noise and the delay it caused.

Your work on comments 7-12 looks great:

  1. get_positive_numeric_env() warning logging (comment 7) — Clean implementation with the warn_on_invalid kwarg. Tests cover all four paths (valid, invalid, non-positive, disabled).

  2. _env.py relocation (comment 8) — The underscore-prefix convention at package root is a pragmatic solution given the real circular import constraint. The comment explaining why it lives there instead of utils/ is appreciated, and filing #610 for the broader re-export discussion is the right call.

  3. Type annotations (comment 9) — All 26 constants annotated across both files. Complete.

  4. @total_ordering investigation (comment 10) — You're correct that it doesn't work with (str, Enum) inheritance. I verified independently — str.__gt__ takes MRO precedence over the decorator-generated method, so LogLevel.CRITICAL > LogLevel.ERROR returns False (alphabetic). The explicit overrides are necessary.

  5. Docstrings (comment 11) — Complete.

  6. get_bool_env() and DISABLE_TESTLEVELSPLIT (comment 12) — This went beyond the fix by also improving the constant name and properly documenting the breaking change in CHANGELOG. The parametrized tests covering 13 input combinations are solid.

Also noticed the BATCHING_REPORTER_ENABLED constant wiring into step_interceptor.py — good consistency with the overall centralization goal.

LGTM — let's unblock the alpha release.


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! 🙂

@oboehmer oboehmer merged commit b5b5935 into release/pyats-integration-v1.1-beta Mar 9, 2026
7 checks passed
@oboehmer oboehmer deleted the feature/512-env-var-alignment-v2 branch March 9, 2026 16:22
aitestino added a commit that referenced this pull request Mar 10, 2026
Merge base branch incorporating PR #599 (logging refactor: VerbosityLevel
-> LogLevel) and PR #554 (dry-run support). Conflict resolution:

- Import merges: combined both our auth-related imports (get_env_var_prefix,
  AUTH_SUCCESS) with the new logging imports (LogLevel, DEFAULT_LOGLEVEL)
- test_main_exit_codes.py: adopted upstream parametrized test structure,
  added PreFlightFailure test case as additional parametrize entry
- Incidental conflicts (subprocess_runner, batching_reporter, robot
  orchestrator): took upstream changes
aitestino added a commit that referenced this pull request Mar 11, 2026
…ution-in-base-class-v2

Brings in critical upstream improvements from v2.0.0a1 release:
- Fail-fast authentication validation (#531)
- Dry-run support for PyATS (#554)
- Clean up stale test artifacts (#526, #571)
- Improved CLI with --verbose and --loglevel flags (#599)
- Better test isolation for parallel execution (#569)
- GitHub templates for issues and PRs (#510)
- Multiple dependency updates and bug fixes

These changes provide the test infrastructure improvements needed for
the defaults resolution feature, particularly the controller auth
validation that addresses test fixture concerns.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Align environment variable naming conventions before FCS

2 participants