Skip to content

Commit 2137644

Browse files
authored
Add unit test suite and code coverage enforcement (#135)
## Summary - Add comprehensive unit test suite (53 tests) covering all modules: CLI entry points, config, doctor checks (GPU, CUDA, memory, NVLink), debug commands, and doctor orchestration - Add pytest-cov dependency and configure 95% minimum coverage threshold with automatic reporting - Fix pytest test path configuration (`[tool.pytest]` → `[tool.pytest.ini_options]`, `tests` → `rapids_cli/tests`) - Add `CLAUDE.md` with project architecture, development commands, code style, and testing guidance - Add SPDX license headers to `dependencies.yaml` ## Test plan - [ ] `pytest` passes all 53 tests - [ ] Coverage meets 95% threshold - [ ] `pre-commit run --all-files` passes 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Release Notes * **Documentation** * Added developer guidance for the repository. * **Tests** * Expanded test coverage for CLI commands, configuration validation, debug output, doctor checks, GPU detection, memory diagnostics, and NVLink status. * **Chores** * Updated project dependencies and testing configuration with coverage reporting and metrics. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent ba0763c commit 2137644

11 files changed

Lines changed: 822 additions & 3 deletions

File tree

CLAUDE.md

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
The RAPIDS CLI is a command-line tool for performing common RAPIDS operations, primarily focused on
8+
health checks (`rapids doctor`) and debugging (`rapids debug`). It uses a plugin system that allows
9+
RAPIDS libraries to register their own health checks via Python entry points.
10+
11+
## Common Commands
12+
13+
### Development Setup
14+
15+
```bash
16+
# Install in editable mode
17+
pip install -e .
18+
19+
# Install with test dependencies
20+
pip install -e .[test]
21+
```
22+
23+
### Testing
24+
25+
```bash
26+
# Run all tests (coverage reporting is automatic via pyproject.toml)
27+
pytest
28+
29+
# Run a specific test file
30+
pytest rapids_cli/tests/test_gpu.py
31+
32+
# Run a specific test function
33+
pytest rapids_cli/tests/test_gpu.py::test_gpu_check_success
34+
35+
# Generate coverage report without running tests
36+
coverage report
37+
38+
# View detailed HTML coverage report
39+
coverage html && open htmlcov/index.html
40+
```
41+
42+
### Linting and Pre-commit
43+
44+
```bash
45+
# Install pre-commit hooks
46+
pre-commit install
47+
48+
# Run all pre-commit checks
49+
pre-commit run --all-files
50+
51+
# Individual linters
52+
black . # Format code
53+
ruff check --fix . # Lint with ruff
54+
mypy rapids_cli/ # Type checking
55+
```
56+
57+
### Running the CLI
58+
59+
```bash
60+
# Run doctor checks
61+
rapids doctor
62+
rapids doctor --verbose
63+
rapids doctor --dry-run
64+
65+
# Run debug command
66+
rapids debug
67+
rapids debug --json
68+
```
69+
70+
## Architecture
71+
72+
### CLI Structure
73+
74+
- **Entry point**: `rapids_cli/cli.py` defines the main CLI group and subcommands using rich-click
75+
- **Doctor command**: `rapids_cli/doctor/doctor.py` contains the health check orchestration logic
76+
- **Debug command**: `rapids_cli/debug/debug.py` gathers system/environment information
77+
- **Checks**: Individual checks live in `rapids_cli/doctor/checks/` (gpu.py, cuda_driver.py, memory.py,
78+
nvlink.py)
79+
80+
### Plugin System
81+
82+
The doctor command discovers and runs checks via Python entry points defined in `pyproject.toml`:
83+
84+
- Entry point group: `rapids_doctor_check`
85+
- Built-in checks are registered in `[project.entry-points.rapids_doctor_check]`
86+
- External packages can register additional checks by adding their own entry points
87+
- Check functions receive `verbose` kwarg and should accept `**kwargs` for forward compatibility
88+
- Checks pass by returning successfully (any return value) and fail by raising exceptions
89+
- Checks can issue warnings using Python's `warnings.warn()` which are caught and displayed
90+
91+
### Check Function Contract
92+
93+
- Accept `verbose=False` and `**kwargs` parameters
94+
- Raise exceptions with helpful error messages for failures
95+
- Return successfully for passing checks (return value is optional string for verbose output)
96+
- Use `warnings.warn()` for non-fatal issues
97+
98+
### Key Dependencies
99+
100+
- `rich` and `rich-click` for terminal output and CLI interface
101+
- `pynvml` (nvidia-ml-py) for GPU information
102+
- `cuda-pathfinder` for locating CUDA installations
103+
- `psutil` for system memory checks
104+
105+
### Configuration
106+
107+
- Package configuration in `pyproject.toml` (build system, dependencies, entry points)
108+
- CLI settings in `rapids_cli/config.yml` (loaded via `config.py`)
109+
- Dependencies managed via `dependencies.yaml` and `rapids-dependency-file-generator`
110+
111+
## Code Style
112+
113+
- Python 3.10+ (minimum version)
114+
- Line length: 120 characters
115+
- Use Google-style docstrings (enforced by ruff with pydocstyle convention)
116+
- Enforce type hints (checked by mypy)
117+
- SPDX license headers required on all files (enforced by pre-commit hook)
118+
- All commits must be signed off with `-s` flag
119+
120+
## Testing Notes
121+
122+
Tests are located in `rapids_cli/tests/`. The test suite runs quickly with 53 tests covering all
123+
modules. GPU-based tests run in CI on actual GPU hardware (L4 instances).
124+
125+
### Coverage Requirements
126+
127+
- Minimum coverage threshold: **95%**
128+
- Coverage is automatically measured when running `pytest`
129+
- Coverage reports are generated in XML format for CI and terminal format for local development
130+
- Test files and `_version.py` are excluded from coverage measurements
131+
132+
## CI/CD
133+
134+
- Pre-commit checks run on all PRs (black, ruff, mypy, shellcheck, etc.)
135+
- Builds both conda packages (noarch: python) and wheels (pure Python)
136+
- Tests run on GPU nodes with CUDA available
137+
- Uses RAPIDS shared workflows for build and test automation

conda/recipes/rapids-cli/recipe.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved.
1+
# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION. All rights reserved.
22
# SPDX-License-Identifier: Apache-2.0
33
schema_version: 1
44

@@ -35,6 +35,7 @@ requirements:
3535
- nvidia-ml-py >=12.0
3636
- packaging
3737
- psutil
38+
- pyyaml
3839
- rich
3940
- rich-click
4041

dependencies.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
14
# Dependency list for https://github.com/rapidsai/dependency-file-generator
25
files:
36
py_run_rapids_cli:
@@ -62,6 +65,7 @@ dependencies:
6265
- cuda-pathfinder >=1.2.3
6366
- packaging
6467
- psutil
68+
- pyyaml
6569
- rich
6670
- rich-click
6771
- output_types: [conda]
@@ -76,3 +80,4 @@ dependencies:
7680
- output_types: [conda, requirements, pyproject]
7781
packages:
7882
- pytest
83+
- pytest-cov

pyproject.toml

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@ dependencies = [
1212
"nvidia-ml-py>=12.0",
1313
"packaging",
1414
"psutil",
15+
"pyyaml",
1516
"rich",
1617
"rich-click",
1718
] # This list was generated by `rapids-dependency-file-generator`. To make changes, edit dependencies.yaml and run `rapids-dependency-file-generator`.
1819

1920
[project.optional-dependencies]
2021
test = [
2122
"pytest",
23+
"pytest-cov",
2224
] # This list was generated by `rapids-dependency-file-generator`. To make changes, edit dependencies.yaml and run `rapids-dependency-file-generator`.
2325

2426
[project.scripts]
@@ -63,8 +65,9 @@ select = [
6365
# PyPI hard limit is 1GiB, but try to keep this as small as possible
6466
max_allowed_size_compressed = '10Mi'
6567

66-
[tool.pytest]
67-
testpaths = ["tests"]
68+
[tool.pytest.ini_options]
69+
testpaths = ["rapids_cli/tests"]
70+
addopts = "--cov=rapids_cli --cov-report=term-missing --cov-report=xml --cov-fail-under=95"
6871

6972
[tool.ruff]
7073
# Exclude a variety of commonly ignored directories.
@@ -140,3 +143,21 @@ convention = "google"
140143
[tool.mypy]
141144
exclude = ["examples", "venv", "ci", "docs", "conftest.py"]
142145
ignore_missing_imports = true
146+
147+
[tool.coverage.run]
148+
source = ["rapids_cli"]
149+
omit = [
150+
"rapids_cli/tests/*",
151+
"rapids_cli/_version.py",
152+
]
153+
154+
[tool.coverage.report]
155+
exclude_lines = [
156+
"pragma: no cover",
157+
"def __repr__",
158+
"raise AssertionError",
159+
"raise NotImplementedError",
160+
"if __name__ == .__main__.:",
161+
"if TYPE_CHECKING:",
162+
"@abstractmethod",
163+
]

rapids_cli/tests/test_cli.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
from unittest.mock import patch
4+
5+
from click.testing import CliRunner
6+
7+
from rapids_cli.cli import debug, doctor, rapids
8+
9+
10+
def test_rapids_cli_help():
11+
"""Test rapids CLI help output."""
12+
runner = CliRunner()
13+
result = runner.invoke(rapids, ["--help"])
14+
assert result.exit_code == 0
15+
assert "The Rapids CLI is a command-line interface for RAPIDS" in result.output
16+
17+
18+
def test_doctor_command_help():
19+
"""Test doctor command help output."""
20+
runner = CliRunner()
21+
result = runner.invoke(rapids, ["doctor", "--help"])
22+
assert result.exit_code == 0
23+
assert "Run health checks" in result.output
24+
25+
26+
def test_debug_command_help():
27+
"""Test debug command help output."""
28+
runner = CliRunner()
29+
result = runner.invoke(rapids, ["debug", "--help"])
30+
assert result.exit_code == 0
31+
assert "Gather debugging information" in result.output
32+
33+
34+
def test_doctor_command_success():
35+
"""Test doctor command with successful checks."""
36+
runner = CliRunner()
37+
with patch("rapids_cli.cli.doctor_check", return_value=True):
38+
result = runner.invoke(rapids, ["doctor"])
39+
assert result.exit_code == 0
40+
41+
42+
def test_doctor_command_failure():
43+
"""Test doctor command with failed checks."""
44+
runner = CliRunner()
45+
with patch("rapids_cli.cli.doctor_check", return_value=False):
46+
result = runner.invoke(rapids, ["doctor"])
47+
assert result.exit_code == 1
48+
assert "Health checks failed" in result.output
49+
50+
51+
def test_doctor_command_verbose():
52+
"""Test doctor command with verbose flag."""
53+
runner = CliRunner()
54+
with patch("rapids_cli.cli.doctor_check", return_value=True) as mock_check:
55+
result = runner.invoke(rapids, ["doctor", "--verbose"])
56+
assert result.exit_code == 0
57+
mock_check.assert_called_once_with(True, False, ())
58+
59+
60+
def test_doctor_command_dry_run():
61+
"""Test doctor command with dry-run flag."""
62+
runner = CliRunner()
63+
with patch("rapids_cli.cli.doctor_check", return_value=True) as mock_check:
64+
result = runner.invoke(rapids, ["doctor", "--dry-run"])
65+
assert result.exit_code == 0
66+
mock_check.assert_called_once_with(False, True, ())
67+
68+
69+
def test_doctor_command_with_filters():
70+
"""Test doctor command with filters."""
71+
runner = CliRunner()
72+
with patch("rapids_cli.cli.doctor_check", return_value=True) as mock_check:
73+
result = runner.invoke(rapids, ["doctor", "cudf", "cuml"])
74+
assert result.exit_code == 0
75+
mock_check.assert_called_once_with(False, False, ("cudf", "cuml"))
76+
77+
78+
def test_debug_command_console():
79+
"""Test debug command with console output."""
80+
runner = CliRunner()
81+
with patch("rapids_cli.cli.run_debug") as mock_debug:
82+
result = runner.invoke(rapids, ["debug"])
83+
assert result.exit_code == 0
84+
mock_debug.assert_called_once_with(output_format="console")
85+
86+
87+
def test_debug_command_json():
88+
"""Test debug command with JSON output."""
89+
runner = CliRunner()
90+
with patch("rapids_cli.cli.run_debug") as mock_debug:
91+
result = runner.invoke(rapids, ["debug", "--json"])
92+
assert result.exit_code == 0
93+
mock_debug.assert_called_once_with(output_format="json")
94+
95+
96+
def test_doctor_standalone():
97+
"""Test doctor command as standalone function."""
98+
runner = CliRunner()
99+
with patch("rapids_cli.cli.doctor_check", return_value=True):
100+
result = runner.invoke(doctor)
101+
assert result.exit_code == 0
102+
103+
104+
def test_debug_standalone():
105+
"""Test debug command as standalone function."""
106+
runner = CliRunner()
107+
with patch("rapids_cli.cli.run_debug"):
108+
result = runner.invoke(debug)
109+
assert result.exit_code == 0

rapids_cli/tests/test_config.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
from rapids_cli.config import config
4+
5+
6+
def test_config_loaded():
7+
"""Test that config is loaded successfully."""
8+
assert config is not None
9+
assert isinstance(config, dict)
10+
11+
12+
def test_config_has_min_supported_versions():
13+
"""Test that config contains minimum supported versions."""
14+
assert "min_supported_versions" in config
15+
assert "gpu_compute_requirement" in config["min_supported_versions"]
16+
17+
18+
def test_config_has_valid_subcommands():
19+
"""Test that config contains valid subcommands."""
20+
assert "valid_subcommands" in config
21+
assert "VALID_SUBCOMMANDS" in config["valid_subcommands"]
22+
23+
24+
def test_config_has_os_requirements():
25+
"""Test that config contains OS requirements."""
26+
assert "os_requirements" in config
27+
assert "VALID_LINUX_OS_VERSIONS" in config["os_requirements"]
28+
assert "OS_TO_MIN_SUPPORTED_VERSION" in config["os_requirements"]
29+
30+
31+
def test_config_has_cudf_section():
32+
"""Test that config contains cuDF section."""
33+
assert "cudf" in config
34+
assert "cuda_requirement" in config["cudf"]
35+
assert "driver_requirement" in config["cudf"]
36+
assert "compute_requirement" in config["cudf"]
37+
assert "links" in config["cudf"]
38+
assert "description" in config["cudf"]
39+
40+
41+
def test_config_has_cuml_section():
42+
"""Test that config contains cuML section."""
43+
assert "cuml" in config
44+
assert "links" in config["cuml"]
45+
assert "description" in config["cuml"]

0 commit comments

Comments
 (0)