Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 137 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

The RAPIDS CLI is a command-line tool for performing common RAPIDS operations, primarily focused on
health checks (`rapids doctor`) and debugging (`rapids debug`). It uses a plugin system that allows
RAPIDS libraries to register their own health checks via Python entry points.

## Common Commands

### Development Setup

```bash
# Install in editable mode
pip install -e .

# Install with test dependencies
pip install -e .[test]
```

### Testing

```bash
# Run all tests (coverage reporting is automatic via pyproject.toml)
pytest

# Run a specific test file
pytest rapids_cli/tests/test_gpu.py

# Run a specific test function
pytest rapids_cli/tests/test_gpu.py::test_gpu_check_success

# Generate coverage report without running tests
coverage report

# View detailed HTML coverage report
coverage html && open htmlcov/index.html
```

### Linting and Pre-commit

```bash
# Install pre-commit hooks
pre-commit install

# Run all pre-commit checks
pre-commit run --all-files

# Individual linters
black . # Format code
ruff check --fix . # Lint with ruff
mypy rapids_cli/ # Type checking
```

### Running the CLI

```bash
# Run doctor checks
rapids doctor
rapids doctor --verbose
rapids doctor --dry-run

# Run debug command
rapids debug
rapids debug --json
```

## Architecture

### CLI Structure

- **Entry point**: `rapids_cli/cli.py` defines the main CLI group and subcommands using rich-click
- **Doctor command**: `rapids_cli/doctor/doctor.py` contains the health check orchestration logic
- **Debug command**: `rapids_cli/debug/debug.py` gathers system/environment information
- **Checks**: Individual checks live in `rapids_cli/doctor/checks/` (gpu.py, cuda_driver.py, memory.py,
nvlink.py)

### Plugin System

The doctor command discovers and runs checks via Python entry points defined in `pyproject.toml`:

- Entry point group: `rapids_doctor_check`
- Built-in checks are registered in `[project.entry-points.rapids_doctor_check]`
- External packages can register additional checks by adding their own entry points
- Check functions receive `verbose` kwarg and should accept `**kwargs` for forward compatibility
- Checks pass by returning successfully (any return value) and fail by raising exceptions
- Checks can issue warnings using Python's `warnings.warn()` which are caught and displayed

### Check Function Contract

- Accept `verbose=False` and `**kwargs` parameters
- Raise exceptions with helpful error messages for failures
- Return successfully for passing checks (return value is optional string for verbose output)
- Use `warnings.warn()` for non-fatal issues

### Key Dependencies

- `rich` and `rich-click` for terminal output and CLI interface
- `pynvml` (nvidia-ml-py) for GPU information
- `cuda-pathfinder` for locating CUDA installations
- `psutil` for system memory checks

### Configuration

- Package configuration in `pyproject.toml` (build system, dependencies, entry points)
- CLI settings in `rapids_cli/config.yml` (loaded via `config.py`)
- Dependencies managed via `dependencies.yaml` and `rapids-dependency-file-generator`

## Code Style

- Python 3.10+ (minimum version)
- Line length: 120 characters
- Use Google-style docstrings (enforced by ruff with pydocstyle convention)
- Enforce type hints (checked by mypy)
- SPDX license headers required on all files (enforced by pre-commit hook)
- All commits must be signed off with `-s` flag

## Testing Notes

Tests are located in `rapids_cli/tests/`. The test suite runs quickly with 53 tests covering all
modules. GPU-based tests run in CI on actual GPU hardware (L4 instances).

### Coverage Requirements

- Minimum coverage threshold: **95%**
- Coverage is automatically measured when running `pytest`
- Coverage reports are generated in XML format for CI and terminal format for local development
- Test files and `_version.py` are excluded from coverage measurements

## CI/CD

- Pre-commit checks run on all PRs (black, ruff, mypy, shellcheck, etc.)
- Builds both conda packages (noarch: python) and wheels (pure Python)
- Tests run on GPU nodes with CUDA available
- Uses RAPIDS shared workflows for build and test automation
3 changes: 2 additions & 1 deletion conda/recipes/rapids-cli/recipe.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved.
# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
schema_version: 1

Expand Down Expand Up @@ -35,6 +35,7 @@ requirements:
- nvidia-ml-py >=12.0
- packaging
- psutil
- pyyaml
- rich
- rich-click

Expand Down
5 changes: 5 additions & 0 deletions dependencies.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

# Dependency list for https://github.com/rapidsai/dependency-file-generator
files:
py_run_rapids_cli:
Expand Down Expand Up @@ -62,6 +65,7 @@ dependencies:
- cuda-pathfinder >=1.2.3
- packaging
- psutil
- pyyaml
- rich
- rich-click
- output_types: [conda]
Expand All @@ -76,3 +80,4 @@ dependencies:
- output_types: [conda, requirements, pyproject]
packages:
- pytest
- pytest-cov
25 changes: 23 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ dependencies = [
"nvidia-ml-py>=12.0",
"packaging",
"psutil",
"pyyaml",
"rich",
"rich-click",
] # This list was generated by `rapids-dependency-file-generator`. To make changes, edit dependencies.yaml and run `rapids-dependency-file-generator`.

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

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

[tool.pytest]
testpaths = ["tests"]
[tool.pytest.ini_options]
testpaths = ["rapids_cli/tests"]
addopts = "--cov=rapids_cli --cov-report=term-missing --cov-report=xml --cov-fail-under=95"

[tool.ruff]
# Exclude a variety of commonly ignored directories.
Expand Down Expand Up @@ -140,3 +143,21 @@ convention = "google"
[tool.mypy]
exclude = ["examples", "venv", "ci", "docs", "conftest.py"]
ignore_missing_imports = true

[tool.coverage.run]
source = ["rapids_cli"]
omit = [
"rapids_cli/tests/*",
"rapids_cli/_version.py",
]

[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"def __repr__",
"raise AssertionError",
"raise NotImplementedError",
"if __name__ == .__main__.:",
"if TYPE_CHECKING:",
"@abstractmethod",
]
109 changes: 109 additions & 0 deletions rapids_cli/tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
from unittest.mock import patch

from click.testing import CliRunner

from rapids_cli.cli import debug, doctor, rapids


def test_rapids_cli_help():
"""Test rapids CLI help output."""
runner = CliRunner()
result = runner.invoke(rapids, ["--help"])
assert result.exit_code == 0
assert "The Rapids CLI is a command-line interface for RAPIDS" in result.output


def test_doctor_command_help():
"""Test doctor command help output."""
runner = CliRunner()
result = runner.invoke(rapids, ["doctor", "--help"])
assert result.exit_code == 0
assert "Run health checks" in result.output


def test_debug_command_help():
"""Test debug command help output."""
runner = CliRunner()
result = runner.invoke(rapids, ["debug", "--help"])
assert result.exit_code == 0
assert "Gather debugging information" in result.output


def test_doctor_command_success():
"""Test doctor command with successful checks."""
runner = CliRunner()
with patch("rapids_cli.cli.doctor_check", return_value=True):
result = runner.invoke(rapids, ["doctor"])
assert result.exit_code == 0


def test_doctor_command_failure():
"""Test doctor command with failed checks."""
runner = CliRunner()
with patch("rapids_cli.cli.doctor_check", return_value=False):
result = runner.invoke(rapids, ["doctor"])
assert result.exit_code == 1
assert "Health checks failed" in result.output


def test_doctor_command_verbose():
"""Test doctor command with verbose flag."""
runner = CliRunner()
with patch("rapids_cli.cli.doctor_check", return_value=True) as mock_check:
result = runner.invoke(rapids, ["doctor", "--verbose"])
assert result.exit_code == 0
mock_check.assert_called_once_with(True, False, ())


def test_doctor_command_dry_run():
"""Test doctor command with dry-run flag."""
runner = CliRunner()
with patch("rapids_cli.cli.doctor_check", return_value=True) as mock_check:
result = runner.invoke(rapids, ["doctor", "--dry-run"])
assert result.exit_code == 0
mock_check.assert_called_once_with(False, True, ())


def test_doctor_command_with_filters():
"""Test doctor command with filters."""
runner = CliRunner()
with patch("rapids_cli.cli.doctor_check", return_value=True) as mock_check:
result = runner.invoke(rapids, ["doctor", "cudf", "cuml"])
assert result.exit_code == 0
mock_check.assert_called_once_with(False, False, ("cudf", "cuml"))


def test_debug_command_console():
"""Test debug command with console output."""
runner = CliRunner()
with patch("rapids_cli.cli.run_debug") as mock_debug:
result = runner.invoke(rapids, ["debug"])
assert result.exit_code == 0
mock_debug.assert_called_once_with(output_format="console")


def test_debug_command_json():
"""Test debug command with JSON output."""
runner = CliRunner()
with patch("rapids_cli.cli.run_debug") as mock_debug:
result = runner.invoke(rapids, ["debug", "--json"])
assert result.exit_code == 0
mock_debug.assert_called_once_with(output_format="json")


def test_doctor_standalone():
"""Test doctor command as standalone function."""
runner = CliRunner()
with patch("rapids_cli.cli.doctor_check", return_value=True):
result = runner.invoke(doctor)
assert result.exit_code == 0


def test_debug_standalone():
"""Test debug command as standalone function."""
runner = CliRunner()
with patch("rapids_cli.cli.run_debug"):
result = runner.invoke(debug)
assert result.exit_code == 0
45 changes: 45 additions & 0 deletions rapids_cli/tests/test_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
from rapids_cli.config import config


def test_config_loaded():
"""Test that config is loaded successfully."""
assert config is not None
assert isinstance(config, dict)


def test_config_has_min_supported_versions():
"""Test that config contains minimum supported versions."""
assert "min_supported_versions" in config
assert "gpu_compute_requirement" in config["min_supported_versions"]


def test_config_has_valid_subcommands():
"""Test that config contains valid subcommands."""
assert "valid_subcommands" in config
assert "VALID_SUBCOMMANDS" in config["valid_subcommands"]


def test_config_has_os_requirements():
"""Test that config contains OS requirements."""
assert "os_requirements" in config
assert "VALID_LINUX_OS_VERSIONS" in config["os_requirements"]
assert "OS_TO_MIN_SUPPORTED_VERSION" in config["os_requirements"]


def test_config_has_cudf_section():
"""Test that config contains cuDF section."""
assert "cudf" in config
assert "cuda_requirement" in config["cudf"]
assert "driver_requirement" in config["cudf"]
assert "compute_requirement" in config["cudf"]
assert "links" in config["cudf"]
assert "description" in config["cudf"]


def test_config_has_cuml_section():
"""Test that config contains cuML section."""
assert "cuml" in config
assert "links" in config["cuml"]
assert "description" in config["cuml"]
Loading