This commit is contained in:
Akram Ben Aissi 2025-06-27 11:39:51 +02:00 committed by GitHub
commit 2907e6413f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 1431 additions and 1 deletions

3
.gitignore vendored
View file

@ -28,3 +28,6 @@ pytest-report.xml
.python-version
CLAUDE.md
.claude/
# Auto-generated build info
llama_stack/cli/build_info.py

View file

@ -11,6 +11,7 @@ from .model import ModelParser
from .stack import StackParser
from .stack.utils import print_subcommand_description
from .verify_download import VerifyDownload
from .version import VersionCommand
class LlamaCLIParser:
@ -34,6 +35,7 @@ class LlamaCLIParser:
StackParser.create(subparsers)
Download.create(subparsers)
VerifyDownload.create(subparsers)
VersionCommand.create(subparsers)
print_subcommand_description(self.parser, subparsers)

View file

@ -28,10 +28,11 @@ class StackListProviders(Subcommand):
return [api.value for api in providable_apis()]
def _add_arguments(self):
choices = self.providable_apis
self.parser.add_argument(
"api",
type=str,
choices=self.providable_apis,
choices=choices if choices else None,
nargs="?",
help="API to list providers for. List all if not specified.",
)

213
llama_stack/cli/version.py Executable file
View file

@ -0,0 +1,213 @@
#!/usr/bin/env python3
# Copyright (c) Meta Platforms, Inc. and affiliates.
# All rights reserved.
#
# This source code is licensed under the terms described in the LICENSE file in
# the root directory of this source tree.
import argparse
import sys
from importlib.metadata import PackageNotFoundError, version
from typing import Any
from llama_stack.cli.subcommand import Subcommand
from llama_stack.cli.table import print_table
class VersionCommand(Subcommand):
"""Display version information for Llama Stack CLI, server, and components"""
def __init__(self, subparsers: argparse._SubParsersAction):
super().__init__()
self.parser = subparsers.add_parser(
"version",
prog="llama version",
description="Display version information for Llama Stack CLI, server, and components",
formatter_class=argparse.RawTextHelpFormatter,
)
self._add_arguments()
self.parser.set_defaults(func=self._run_version_command)
def _add_arguments(self):
self.parser.add_argument(
"--format",
choices=["table", "json"],
default="table",
help="Output format (default: table)",
)
self.parser.add_argument(
"--components",
action="store_true",
help="Include available components/providers information",
)
def _get_package_version(self, package_name: str) -> str:
"""Get version of a package, return 'unknown' if not found"""
try:
return version(package_name)
except PackageNotFoundError:
return "unknown"
def _get_build_info(self) -> dict:
"""Get build information from build_info.py"""
build_info = {
"commit_hash": "unknown",
"commit_date": "unknown",
"branch": "unknown",
"tag": "unknown",
"build_timestamp": "unknown",
}
try:
from .build_info import BUILD_INFO
build_info.update(
{
"commit_hash": BUILD_INFO.get("git_commit", "unknown"),
"commit_date": BUILD_INFO.get("git_commit_date", "unknown"),
"branch": BUILD_INFO.get("git_branch", "unknown"),
"tag": BUILD_INFO.get("git_tag", "unknown"),
"build_timestamp": BUILD_INFO.get("build_timestamp", "unknown"),
}
)
except ImportError:
# build_info.py not available, use default values
pass
return build_info
def _get_components_info(self) -> list:
"""Get information about available components/providers"""
components = []
try:
# Lazy import to avoid loading heavy dependencies unless needed
from llama_stack.distribution.distribution import get_provider_registry
registry = get_provider_registry()
# Group providers by API
api_providers: dict[str, list[Any]] = {}
for api, providers_dict in registry.items():
api_name = api.value
if api_name not in api_providers:
api_providers[api_name] = []
for provider_spec in providers_dict.values():
api_providers[api_name].append(provider_spec)
# Create component info
for api_str, providers in api_providers.items():
for provider in providers:
provider_type = getattr(provider, "provider_type", "unknown")
adapter_type = getattr(provider, "adapter_type", None)
# Determine component type
if provider_type.startswith("inline::"):
component_type = "inline"
component_name = provider_type.replace("inline::", "")
elif provider_type.startswith("remote::"):
component_type = "remote"
component_name = provider_type.replace("remote::", "")
elif adapter_type:
component_type = "remote"
component_name = adapter_type
else:
component_type = "unknown"
component_name = provider_type
components.append(
{
"api": api_str,
"component": component_name,
"type": component_type,
"provider_type": provider_type,
}
)
except Exception as e:
print(f"Warning: Could not load components information: {e}", file=sys.stderr)
return components
def _run_version_command(self, args: argparse.Namespace) -> None:
"""Execute the version command"""
import json
# Get version information
llama_stack_version = self._get_package_version("llama-stack")
llama_stack_client_version = self._get_package_version("llama-stack-client")
python_version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
# Get build information
build_info = self._get_build_info()
version_info = {
"llama_stack_version": llama_stack_version,
"llama_stack_client_version": llama_stack_client_version,
"python_version": python_version,
"git_commit": build_info["commit_hash"],
"git_commit_date": build_info["commit_date"],
"git_branch": build_info["branch"],
"git_tag": build_info["tag"],
"build_timestamp": build_info["build_timestamp"],
}
if args.format == "json":
output = version_info.copy()
if args.components:
output["components"] = self._get_components_info()
print(json.dumps(output, indent=2))
else:
# Table format
print("Llama Stack Version Information")
print("=" * 50)
# Version table
version_rows = [
["Llama Stack", llama_stack_version],
["Llama Stack Client", llama_stack_client_version],
["Python", python_version],
]
print_table(version_rows, ["Component", "Version"])
print("\nBuild Information")
print("-" * 30)
# Build info table
build_rows = [
["Git Commit", build_info["commit_hash"]],
["Commit Date", build_info["commit_date"]],
["Git Branch", build_info["branch"]],
["Git Tag", build_info["tag"]],
["Build Timestamp", build_info["build_timestamp"]],
]
print_table(build_rows, ["Property", "Value"])
if args.components:
print("\nAvailable Components")
print("-" * 30)
components = self._get_components_info()
if components:
# Group by API for better display
api_groups: dict[str, list[dict[str, str]]] = {}
for comp in components:
api = comp["api"]
if api not in api_groups:
api_groups[api] = []
api_groups[api].append(comp)
for api, comps in sorted(api_groups.items()):
print(f"\n{api.upper()} API:")
comp_rows = []
for comp in sorted(comps, key=lambda x: x["component"]):
comp_rows.append([comp["component"], comp["type"], comp["provider_type"]])
# Print with manual indentation since print_table doesn't support indent
print(" Component Type Provider Type")
print(" " + "-" * 50)
for row in comp_rows:
print(f" {row[0]:<20} {row[1]:<8} {row[2]}")
print()
else:
print("No components information available")

47
scripts/README.md Normal file
View file

@ -0,0 +1,47 @@
# Build Scripts
This directory contains scripts used during the build process.
## generate_build_info.py
This script generates `llama_stack/cli/build_info.py` with hardcoded git information at build time. This ensures that version information is captured at build time rather than being read dynamically at runtime.
### Usage
The script is automatically run during the build process via the custom `BuildWithBuildInfo` class in `setup.py`. You can also run it manually:
```bash
python scripts/generate_build_info.py
```
### Generated Information
The script captures the following information:
- Git commit hash (short form)
- Git commit date
- Git branch name
- Git tag (latest)
- Build timestamp
### CI/CD Integration
The script handles common CI/CD scenarios:
- Detached HEAD state (common in CI environments)
- Missing git information (fallback to environment variables)
- Git not available (fallback to "unknown" values)
### Environment Variables
When running in CI/CD environments where the branch name might not be available via git (detached HEAD), the script checks these environment variables:
- `GITHUB_REF_NAME` (GitHub Actions)
- `CI_COMMIT_REF_NAME` (GitLab CI)
- `BUILDKITE_BRANCH` (Buildkite)
- `TRAVIS_BRANCH` (Travis CI)
### Build Integration
The build info generation is integrated into the build process via:
1. `setup.py` - Custom build command that runs the script before building
2. `pyproject.toml` - Standard Python package configuration
This ensures that every build includes up-to-date git information without requiring runtime git access.

151
scripts/generate_build_info.py Executable file
View file

@ -0,0 +1,151 @@
#!/usr/bin/env python3
# Copyright (c) Meta Platforms, Inc. and affiliates.
# All rights reserved.
#
# This source code is licensed under the terms described in the LICENSE file in
# the root directory of this source tree.
# This file is auto-generated during build time
# DO NOT EDIT MANUALLY
import subprocess
import sys
from datetime import UTC, datetime
from pathlib import Path
def get_git_info():
"""Get git information for build"""
git_info = {
"git_commit": "unknown",
"git_commit_date": "unknown",
"git_branch": "unknown",
"git_tag": "unknown",
}
try:
# Get current directory - assume script is in scripts/ directory
repo_root = Path(__file__).parent.parent
# Get commit hash
result = subprocess.run(
["git", "rev-parse", "HEAD"],
cwd=repo_root,
capture_output=True,
text=True,
timeout=10,
)
if result.returncode == 0:
git_info["git_commit"] = result.stdout.strip()[:12] # Short hash
# Get commit date
result = subprocess.run(
["git", "log", "-1", "--format=%ci"],
cwd=repo_root,
capture_output=True,
text=True,
timeout=10,
)
if result.returncode == 0:
git_info["git_commit_date"] = result.stdout.strip()
# Get current branch
result = subprocess.run(
["git", "rev-parse", "--abbrev-ref", "HEAD"],
cwd=repo_root,
capture_output=True,
text=True,
timeout=10,
)
if result.returncode == 0:
branch = result.stdout.strip()
# If we're in detached HEAD state (common in CI), try to get branch from env
if branch == "HEAD":
# Try common CI environment variables
import os
branch = (
os.getenv("GITHUB_REF_NAME")
or os.getenv("CI_COMMIT_REF_NAME") # GitLab
or os.getenv("BUILDKITE_BRANCH")
or os.getenv("TRAVIS_BRANCH")
or "HEAD"
)
git_info["git_branch"] = branch
# Get latest tag
result = subprocess.run(
["git", "describe", "--tags", "--abbrev=0"],
cwd=repo_root,
capture_output=True,
text=True,
timeout=10,
)
if result.returncode == 0:
git_info["git_tag"] = result.stdout.strip()
else:
# If no tags, try to get the closest tag with distance
result = subprocess.run(
["git", "describe", "--tags", "--always"],
cwd=repo_root,
capture_output=True,
text=True,
timeout=10,
)
if result.returncode == 0:
git_info["git_tag"] = result.stdout.strip()
except (subprocess.TimeoutExpired, subprocess.SubprocessError, FileNotFoundError) as e:
print(f"Warning: Could not get git information: {e}", file=sys.stderr)
return git_info
def generate_build_info_file():
"""Generate the build_info.py file with current git information"""
git_info = get_git_info()
# Add build timestamp
build_timestamp = datetime.now(UTC).isoformat()
# Get the target file path
script_dir = Path(__file__).parent
target_file = script_dir.parent / "llama_stack" / "cli" / "build_info.py"
# Generate the content
content = f"""#!/usr/bin/env python3
# Copyright (c) Meta Platforms, Inc. and affiliates.
# All rights reserved.
#
# This source code is licensed under the terms described in the LICENSE file in
# the root directory of this source tree.
# This file is auto-generated during build time
# DO NOT EDIT MANUALLY
BUILD_INFO = {{
"git_commit": "{git_info["git_commit"]}",
"git_commit_date": "{git_info["git_commit_date"]}",
"git_branch": "{git_info["git_branch"]}",
"git_tag": "{git_info["git_tag"]}",
"build_timestamp": "{build_timestamp}",
}}
"""
# Write the file
try:
target_file.write_text(content)
print(f"Generated build info file: {target_file}")
print(f"Git commit: {git_info['git_commit']}")
print(f"Git branch: {git_info['git_branch']}")
print(f"Git tag: {git_info['git_tag']}")
print(f"Build timestamp: {build_timestamp}")
except Exception as e:
print(f"Error writing build info file: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
generate_build_info_file()

53
setup.py Executable file
View file

@ -0,0 +1,53 @@
#!/usr/bin/env python3
# Copyright (c) Meta Platforms, Inc. and affiliates.
# All rights reserved.
#
# This source code is licensed under the terms described in the LICENSE file in
# the root directory of this source tree.
import subprocess
import sys
from pathlib import Path
from setuptools import setup
from setuptools.command.build_py import build_py
class BuildWithBuildInfo(build_py):
"""Custom build command that generates build info before building"""
def run(self):
# Generate build info before building
self.generate_build_info()
# Run the standard build
super().run()
def generate_build_info(self):
"""Generate build_info.py with current git information"""
script_path = Path(__file__).parent / "scripts" / "generate_build_info.py"
try:
# Run the build info generation script
result = subprocess.run(
[sys.executable, str(script_path)],
check=True,
capture_output=True,
text=True,
)
print("Build info generation completed successfully")
if result.stdout:
print(result.stdout)
except subprocess.CalledProcessError as e:
print(f"Warning: Failed to generate build info: {e}")
if e.stderr:
print(f"Error output: {e.stderr}")
# Don't fail the build, just continue with default values
if __name__ == "__main__":
setup(
cmdclass={
"build_py": BuildWithBuildInfo,
},
)

95
tests/unit/cli/README.md Normal file
View file

@ -0,0 +1,95 @@
# CLI Unit Tests
This directory contains unit tests for the Llama Stack CLI commands.
## Test Files
### `test_version.py`
Comprehensive unit tests for the `llama version` command using pytest and mocking. These tests cover:
- Package version retrieval
- Build information handling
- Component information retrieval
- JSON and table output formats
- Error handling scenarios
- Component type detection
**Requirements:** pytest, unittest.mock
**Run with:** `pytest tests/unit/cli/test_version.py -v`
### `test_version_simple.py`
Simple unit tests that can run without external dependencies. These tests verify:
- File structure and existence
- Code structure and imports
- Logic validation
- Configuration correctness
**Requirements:** None (uses only standard library)
**Run with:** `python tests/unit/cli/test_version_simple.py`
### `test_version_integration.py`
Integration tests that test the actual CLI command execution. These tests:
- Execute the actual CLI commands
- Verify command-line interface
- Test real output formats
- Validate build script execution
**Requirements:** Full Llama Stack environment
**Run with:** `python tests/unit/cli/test_version_integration.py`
### `test_stack_config.py`
Tests for stack configuration parsing and upgrading.
## Running Tests
### Run All CLI Tests
```bash
# With pytest (if available)
pytest tests/unit/cli/ -v
# Simple tests only (no dependencies)
python tests/unit/cli/test_version_simple.py
```
### Run Specific Tests
```bash
# Run version command tests
python tests/unit/cli/test_version_simple.py
# Run integration tests (requires full environment)
python tests/unit/cli/test_version_integration.py
```
## Test Coverage
The version command tests cover:
- ✅ Package version detection
- ✅ Build information retrieval
- ✅ Component discovery and listing
- ✅ JSON output format
- ✅ Table output format
- ✅ Error handling
- ✅ CLI integration
- ✅ Build script functionality
- ✅ File structure validation
## Adding New Tests
When adding new CLI commands or functionality:
1. Create unit tests following the pattern in `test_version.py`
2. Add simple validation tests in a `test_<command>_simple.py` file
3. Consider integration tests for end-to-end validation
4. Update this README with the new test information
## Notes
- Simple tests are preferred for CI/CD as they have no external dependencies
- Integration tests are useful for local development and full validation
- Mock-based tests provide comprehensive coverage of edge cases

View file

@ -0,0 +1,414 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
# All rights reserved.
#
# This source code is licensed under the terms described in the LICENSE file in
# the root directory of this source tree.
import argparse
import json
import sys
from io import StringIO
from unittest.mock import Mock, patch
import pytest
from llama_stack.cli.version import VersionCommand
class TestVersionCommand:
"""Test suite for the VersionCommand class"""
@pytest.fixture
def version_command(self):
"""Create a VersionCommand instance for testing"""
# Create a mock subparsers object
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()
return VersionCommand(subparsers)
@pytest.fixture
def mock_build_info(self):
"""Mock build info data"""
return {
"git_commit": "abc123def456",
"git_commit_date": "2025-01-15 10:30:00 -0800",
"git_branch": "main",
"git_tag": "v0.2.12",
"build_timestamp": "2025-01-15T18:30:00.123456+00:00",
}
@pytest.fixture
def mock_components_info(self):
"""Mock components info data"""
return [
{
"api": "inference",
"component": "meta-reference",
"type": "inline",
"provider_type": "inline::meta-reference",
},
{
"api": "inference",
"component": "vllm",
"type": "inline",
"provider_type": "inline::vllm",
},
{
"api": "inference",
"component": "ollama",
"type": "remote",
"provider_type": "remote::ollama",
},
{
"api": "safety",
"component": "llama-guard",
"type": "inline",
"provider_type": "inline::llama-guard",
},
]
def test_get_package_version_existing_package(self, version_command):
"""Test getting version of an existing package"""
with patch("llama_stack.cli.version.version") as mock_version:
mock_version.return_value = "1.2.3"
result = version_command._get_package_version("test-package")
assert result == "1.2.3"
mock_version.assert_called_once_with("test-package")
def test_get_package_version_missing_package(self, version_command):
"""Test getting version of a non-existent package"""
with patch("llama_stack.cli.version.version") as mock_version:
from importlib.metadata import PackageNotFoundError
mock_version.side_effect = PackageNotFoundError()
result = version_command._get_package_version("non-existent-package")
assert result == "unknown"
def test_get_build_info_with_build_info_module(self, version_command, mock_build_info):
"""Test getting build info when build_info module is available"""
mock_build_info_dict = {
"git_commit": mock_build_info["git_commit"],
"git_commit_date": mock_build_info["git_commit_date"],
"git_branch": mock_build_info["git_branch"],
"git_tag": mock_build_info["git_tag"],
"build_timestamp": mock_build_info["build_timestamp"],
}
with patch("llama_stack.cli.version.BUILD_INFO", mock_build_info_dict):
result = version_command._get_build_info()
assert result["commit_hash"] == mock_build_info["git_commit"]
assert result["commit_date"] == mock_build_info["git_commit_date"]
assert result["branch"] == mock_build_info["git_branch"]
assert result["tag"] == mock_build_info["git_tag"]
assert result["build_timestamp"] == mock_build_info["build_timestamp"]
def test_get_build_info_without_build_info_module(self, version_command):
"""Test getting build info when build_info module is not available"""
with patch("llama_stack.cli.version.BUILD_INFO", side_effect=ImportError()):
result = version_command._get_build_info()
assert result["commit_hash"] == "unknown"
assert result["commit_date"] == "unknown"
assert result["branch"] == "unknown"
assert result["tag"] == "unknown"
assert result["build_timestamp"] == "unknown"
def test_get_components_info_success(self, version_command, mock_components_info):
"""Test getting components info successfully"""
# Mock the provider registry
mock_registry = {
"inference": {
"inline::meta-reference": Mock(
api=Mock(value="inference"),
provider_type="inline::meta-reference",
adapter_type=None,
),
"inline::vllm": Mock(
api=Mock(value="inference"),
provider_type="inline::vllm",
adapter_type=None,
),
"remote::ollama": Mock(
api=Mock(value="inference"),
provider_type="remote::ollama",
adapter_type="ollama",
),
},
"safety": {
"inline::llama-guard": Mock(
api=Mock(value="safety"),
provider_type="inline::llama-guard",
adapter_type=None,
),
},
}
with patch("llama_stack.cli.version.get_provider_registry") as mock_get_registry:
mock_get_registry.return_value = mock_registry
result = version_command._get_components_info()
# Should have 4 components
assert len(result) == 4
# Check that all expected components are present
component_names = [comp["component"] for comp in result]
assert "meta-reference" in component_names
assert "vllm" in component_names
assert "ollama" in component_names
assert "llama-guard" in component_names
def test_get_components_info_exception(self, version_command):
"""Test getting components info when an exception occurs"""
with patch("llama_stack.cli.version.get_provider_registry") as mock_get_registry:
mock_get_registry.side_effect = Exception("Test error")
# Capture stderr to check warning message
with patch("sys.stderr", new_callable=StringIO) as mock_stderr:
result = version_command._get_components_info()
assert result == []
assert "Warning: Could not load components information" in mock_stderr.getvalue()
def test_run_version_command_table_format(self, version_command, mock_build_info):
"""Test running version command with table format"""
args = argparse.Namespace(format="table", components=False)
with (
patch.object(version_command, "_get_package_version") as mock_get_version,
patch.object(version_command, "_get_build_info") as mock_get_build_info,
patch("llama_stack.cli.version.print_table") as mock_print_table,
patch("builtins.print") as mock_print,
):
mock_get_version.side_effect = lambda pkg: {
"llama-stack": "0.2.12",
"llama-stack-client": "0.2.12",
}.get(pkg, "unknown")
mock_get_build_info.return_value = {
"commit_hash": mock_build_info["git_commit"],
"commit_date": mock_build_info["git_commit_date"],
"branch": mock_build_info["git_branch"],
"tag": mock_build_info["git_tag"],
"build_timestamp": mock_build_info["build_timestamp"],
}
version_command._run_version_command(args)
# Check that print was called with headers
mock_print.assert_any_call("Llama Stack Version Information")
mock_print.assert_any_call("=" * 50)
mock_print.assert_any_call("\nBuild Information")
mock_print.assert_any_call("-" * 30)
# Check that print_table was called twice (version and build info)
assert mock_print_table.call_count == 2
def test_run_version_command_json_format(self, version_command, mock_build_info):
"""Test running version command with JSON format"""
args = argparse.Namespace(format="json", components=False)
with (
patch.object(version_command, "_get_package_version") as mock_get_version,
patch.object(version_command, "_get_build_info") as mock_get_build_info,
patch("builtins.print") as mock_print,
):
mock_get_version.side_effect = lambda pkg: {
"llama-stack": "0.2.12",
"llama-stack-client": "0.2.12",
}.get(pkg, "unknown")
mock_get_build_info.return_value = {
"commit_hash": mock_build_info["git_commit"],
"commit_date": mock_build_info["git_commit_date"],
"branch": mock_build_info["git_branch"],
"tag": mock_build_info["git_tag"],
"build_timestamp": mock_build_info["build_timestamp"],
}
version_command._run_version_command(args)
# Check that JSON was printed
mock_print.assert_called_once()
printed_output = mock_print.call_args[0][0]
# Parse the JSON to verify it's valid and contains expected fields
json_output = json.loads(printed_output)
assert json_output["llama_stack_version"] == "0.2.12"
assert json_output["llama_stack_client_version"] == "0.2.12"
assert json_output["git_commit"] == mock_build_info["git_commit"]
assert json_output["git_branch"] == mock_build_info["git_branch"]
assert json_output["build_timestamp"] == mock_build_info["build_timestamp"]
def test_run_version_command_with_components_table(self, version_command, mock_build_info, mock_components_info):
"""Test running version command with components in table format"""
args = argparse.Namespace(format="table", components=True)
with (
patch.object(version_command, "_get_package_version") as mock_get_version,
patch.object(version_command, "_get_build_info") as mock_get_build_info,
patch.object(version_command, "_get_components_info") as mock_get_components_info,
patch("builtins.print") as mock_print,
):
mock_get_version.side_effect = lambda pkg: {
"llama-stack": "0.2.12",
"llama-stack-client": "0.2.12",
}.get(pkg, "unknown")
mock_get_build_info.return_value = {
"commit_hash": mock_build_info["git_commit"],
"commit_date": mock_build_info["git_commit_date"],
"branch": mock_build_info["git_branch"],
"tag": mock_build_info["git_tag"],
"build_timestamp": mock_build_info["build_timestamp"],
}
mock_get_components_info.return_value = mock_components_info
version_command._run_version_command(args)
# Check that components section was printed
mock_print.assert_any_call("\nAvailable Components")
mock_print.assert_any_call("-" * 30)
# Check that API sections were printed
mock_print.assert_any_call("\nINFERENCE API:")
mock_print.assert_any_call("\nSAFETY API:")
def test_run_version_command_with_components_json(self, version_command, mock_build_info, mock_components_info):
"""Test running version command with components in JSON format"""
args = argparse.Namespace(format="json", components=True)
with (
patch.object(version_command, "_get_package_version") as mock_get_version,
patch.object(version_command, "_get_build_info") as mock_get_build_info,
patch.object(version_command, "_get_components_info") as mock_get_components_info,
patch("builtins.print") as mock_print,
):
mock_get_version.side_effect = lambda pkg: {
"llama-stack": "0.2.12",
"llama-stack-client": "0.2.12",
}.get(pkg, "unknown")
mock_get_build_info.return_value = {
"commit_hash": mock_build_info["git_commit"],
"commit_date": mock_build_info["git_commit_date"],
"branch": mock_build_info["git_branch"],
"tag": mock_build_info["git_tag"],
"build_timestamp": mock_build_info["build_timestamp"],
}
mock_get_components_info.return_value = mock_components_info
version_command._run_version_command(args)
# Check that JSON was printed
mock_print.assert_called_once()
printed_output = mock_print.call_args[0][0]
# Parse the JSON to verify it contains components
json_output = json.loads(printed_output)
assert "components" in json_output
assert len(json_output["components"]) == 4
# Check that components have expected structure
component = json_output["components"][0]
assert "api" in component
assert "component" in component
assert "type" in component
assert "provider_type" in component
def test_run_version_command_no_components_available(self, version_command, mock_build_info):
"""Test running version command when no components are available"""
args = argparse.Namespace(format="table", components=True)
with (
patch.object(version_command, "_get_package_version") as mock_get_version,
patch.object(version_command, "_get_build_info") as mock_get_build_info,
patch.object(version_command, "_get_components_info") as mock_get_components_info,
patch("builtins.print") as mock_print,
):
mock_get_version.side_effect = lambda pkg: {
"llama-stack": "0.2.12",
"llama-stack-client": "0.2.12",
}.get(pkg, "unknown")
mock_get_build_info.return_value = {
"commit_hash": mock_build_info["git_commit"],
"commit_date": mock_build_info["git_commit_date"],
"branch": mock_build_info["git_branch"],
"tag": mock_build_info["git_tag"],
"build_timestamp": mock_build_info["build_timestamp"],
}
mock_get_components_info.return_value = []
version_command._run_version_command(args)
# Check that "no components" message was printed
mock_print.assert_any_call("No components information available")
def test_python_version_format(self, version_command):
"""Test that Python version is formatted correctly"""
args = argparse.Namespace(format="json", components=False)
with (
patch.object(version_command, "_get_package_version") as mock_get_version,
patch.object(version_command, "_get_build_info") as mock_get_build_info,
patch("builtins.print") as mock_print,
):
mock_get_version.return_value = "0.2.12"
mock_get_build_info.return_value = {
"commit_hash": "abc123",
"commit_date": "2025-01-15",
"branch": "main",
"tag": "v0.2.12",
"build_timestamp": "2025-01-15T18:30:00+00:00",
}
version_command._run_version_command(args)
printed_output = mock_print.call_args[0][0]
json_output = json.loads(printed_output)
# Check that Python version matches current Python version
expected_python_version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
assert json_output["python_version"] == expected_python_version
def test_component_type_detection(self, version_command):
"""Test that component types are detected correctly"""
# Test inline provider
mock_registry = {
"inference": {
"inline::test": Mock(
api=Mock(value="inference"),
provider_type="inline::test",
adapter_type=None,
),
"remote::test": Mock(
api=Mock(value="inference"),
provider_type="remote::test",
adapter_type=None,
),
"adapter-test": Mock(
api=Mock(value="inference"),
provider_type="adapter-test",
adapter_type="test-adapter",
),
},
}
with patch("llama_stack.cli.version.get_provider_registry") as mock_get_registry:
mock_get_registry.return_value = mock_registry
result = version_command._get_components_info()
# Find components by provider type to avoid conflicts
inline_components = [comp for comp in result if comp["provider_type"] == "inline::test"]
remote_components = [comp for comp in result if comp["provider_type"] == "remote::test"]
adapter_components = [comp for comp in result if comp["provider_type"] == "adapter-test"]
assert len(inline_components) == 1
assert inline_components[0]["type"] == "inline"
assert len(remote_components) == 1
assert remote_components[0]["type"] == "remote"
assert len(adapter_components) == 1
assert adapter_components[0]["type"] == "remote"

View file

@ -0,0 +1,220 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
# All rights reserved.
#
# This source code is licensed under the terms described in the LICENSE file in
# the root directory of this source tree.
"""
Integration tests for the version command that can be run independently.
These tests verify the basic functionality without requiring complex mocking.
"""
import json
import subprocess
import sys
from pathlib import Path
def test_version_command_help():
"""Test that the version command help works"""
try:
result = subprocess.run(
[sys.executable, "-m", "llama_stack.cli.llama", "version", "--help"],
capture_output=True,
text=True,
timeout=30,
cwd=Path(__file__).parent.parent.parent.parent,
)
assert result.returncode == 0
assert "Display version information" in result.stdout
assert "--format" in result.stdout
assert "--components" in result.stdout
print("✓ Version command help test passed")
except subprocess.TimeoutExpired:
print("✗ Version command help test timed out")
raise
except Exception as e:
print(f"✗ Version command help test failed: {e}")
raise
def test_version_command_basic():
"""Test basic version command execution"""
try:
result = subprocess.run(
[sys.executable, "-m", "llama_stack.cli.llama", "version", "--format", "json"],
capture_output=True,
text=True,
timeout=30,
cwd=Path(__file__).parent.parent.parent.parent,
)
assert result.returncode == 0
# Parse JSON output
json_output = json.loads(result.stdout.split("\n")[-2]) # Get last non-empty line
# Check required fields
required_fields = [
"llama_stack_version",
"llama_stack_client_version",
"python_version",
"git_commit",
"git_commit_date",
"git_branch",
"git_tag",
"build_timestamp",
]
for field in required_fields:
assert field in json_output, f"Missing field: {field}"
# Check that values are not empty (except for potentially unknown values)
assert json_output["python_version"] != ""
assert "." in json_output["python_version"] # Should be in format x.y.z
print("✓ Version command basic test passed")
print(f" Llama Stack version: {json_output['llama_stack_version']}")
print(f" Python version: {json_output['python_version']}")
print(f" Git commit: {json_output['git_commit']}")
except subprocess.TimeoutExpired:
print("✗ Version command basic test timed out")
raise
except Exception as e:
print(f"✗ Version command basic test failed: {e}")
raise
def test_version_command_with_components():
"""Test version command with components flag"""
try:
result = subprocess.run(
[sys.executable, "-m", "llama_stack.cli.llama", "version", "--format", "json", "--components"],
capture_output=True,
text=True,
timeout=60, # Longer timeout for components
cwd=Path(__file__).parent.parent.parent.parent,
)
assert result.returncode == 0
# Parse JSON output
json_output = json.loads(result.stdout.split("\n")[-2]) # Get last non-empty line
# Check that components field exists
assert "components" in json_output
assert isinstance(json_output["components"], list)
# If components exist, check their structure
if json_output["components"]:
component = json_output["components"][0]
required_component_fields = ["api", "component", "type", "provider_type"]
for field in required_component_fields:
assert field in component, f"Missing component field: {field}"
print("✓ Version command with components test passed")
print(f" Found {len(json_output['components'])} components")
except subprocess.TimeoutExpired:
print("✗ Version command with components test timed out")
raise
except Exception as e:
print(f"✗ Version command with components test failed: {e}")
raise
def test_build_info_structure():
"""Test that build_info.py has the correct structure"""
try:
# Import build info directly
build_info_path = Path(__file__).parent.parent.parent.parent / "llama_stack" / "cli" / "build_info.py"
if build_info_path.exists():
# Read the file content
content = build_info_path.read_text()
# Check that it contains BUILD_INFO
assert "BUILD_INFO" in content
# Check that it has the expected fields
expected_fields = [
"git_commit",
"git_commit_date",
"git_branch",
"git_tag",
"build_timestamp",
]
for field in expected_fields:
assert field in content, f"Missing field in build_info.py: {field}"
print("✓ Build info structure test passed")
else:
print("! Build info file not found - this is expected in development")
except Exception as e:
print(f"✗ Build info structure test failed: {e}")
raise
def test_build_script_execution():
"""Test that the build script can be executed"""
try:
script_path = Path(__file__).parent.parent.parent.parent / "scripts" / "generate_build_info.py"
if script_path.exists():
result = subprocess.run(
[sys.executable, str(script_path)],
capture_output=True,
text=True,
timeout=30,
cwd=script_path.parent.parent,
)
assert result.returncode == 0
assert "Generated build info file" in result.stdout
print("✓ Build script execution test passed")
else:
print("! Build script not found")
except subprocess.TimeoutExpired:
print("✗ Build script execution test timed out")
raise
except Exception as e:
print(f"✗ Build script execution test failed: {e}")
raise
if __name__ == "__main__":
"""Run integration tests when executed directly"""
print("Running version command integration tests...")
tests = [
test_version_command_help,
test_version_command_basic,
test_version_command_with_components,
test_build_info_structure,
test_build_script_execution,
]
passed = 0
failed = 0
for test in tests:
try:
test()
passed += 1
except Exception as e:
print(f"Test {test.__name__} failed: {e}")
failed += 1
print(f"\nResults: {passed} passed, {failed} failed")
if failed > 0:
sys.exit(1)
else:
print("All integration tests passed!")

View file

@ -0,0 +1,231 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
# All rights reserved.
#
# This source code is licensed under the terms described in the LICENSE file in
# the root directory of this source tree.
"""
Simple unit tests for the version command that can be run independently.
These tests focus on testing individual methods and functionality.
"""
import json
import sys
from pathlib import Path
def test_build_info_file_structure():
"""Test that build_info.py has the correct structure when it exists"""
build_info_path = Path(__file__).parent.parent.parent.parent / "llama_stack" / "cli" / "build_info.py"
if build_info_path.exists():
content = build_info_path.read_text()
# Check that it contains BUILD_INFO
assert "BUILD_INFO" in content, "build_info.py should contain BUILD_INFO"
# Check that it has the expected fields
expected_fields = [
"git_commit",
"git_commit_date",
"git_branch",
"git_tag",
"build_timestamp",
]
for field in expected_fields:
assert field in content, f"Missing field in build_info.py: {field}"
print("✓ Build info file structure test passed")
return True
else:
print("! Build info file not found - this is expected in development")
return True
def test_build_script_exists():
"""Test that the build script exists and has the correct structure"""
script_path = Path(__file__).parent.parent.parent.parent / "scripts" / "generate_build_info.py"
assert script_path.exists(), "Build script should exist"
content = script_path.read_text()
# Check for key functions
assert "def get_git_info" in content, "Build script should have get_git_info function"
assert "def generate_build_info_file" in content, "Build script should have generate_build_info_file function"
assert "BUILD_INFO" in content, "Build script should reference BUILD_INFO"
print("✓ Build script exists and has correct structure")
return True
def test_version_module_structure():
"""Test that the version module has the correct structure"""
version_path = Path(__file__).parent.parent.parent.parent / "llama_stack" / "cli" / "version.py"
assert version_path.exists(), "Version module should exist"
content = version_path.read_text()
# Check for key classes and methods
assert "class VersionCommand" in content, "Should have VersionCommand class"
assert "def _get_package_version" in content, "Should have _get_package_version method"
assert "def _get_build_info" in content, "Should have _get_build_info method"
assert "def _get_components_info" in content, "Should have _get_components_info method"
assert "def _run_version_command" in content, "Should have _run_version_command method"
# Check for proper imports
assert "from llama_stack.cli.subcommand import Subcommand" in content, "Should import Subcommand"
assert "from llama_stack.distribution.distribution import get_provider_registry" in content, (
"Should import get_provider_registry"
)
print("✓ Version module structure test passed")
return True
def test_cli_integration():
"""Test that the version command is properly integrated into the CLI"""
llama_cli_path = Path(__file__).parent.parent.parent.parent / "llama_stack" / "cli" / "llama.py"
assert llama_cli_path.exists(), "Main CLI module should exist"
content = llama_cli_path.read_text()
# Check that version command is imported and added
assert "from .version import VersionCommand" in content, "Should import VersionCommand"
assert "VersionCommand.create(subparsers)" in content, "Should add VersionCommand to subparsers"
print("✓ CLI integration test passed")
return True
def test_gitignore_entry():
"""Test that build_info.py is properly ignored in git"""
gitignore_path = Path(__file__).parent.parent.parent.parent / ".gitignore"
if gitignore_path.exists():
content = gitignore_path.read_text()
assert "llama_stack/cli/build_info.py" in content, "build_info.py should be in .gitignore"
print("✓ Gitignore entry test passed")
return True
else:
print("! .gitignore not found")
return True
def test_component_type_detection_logic():
"""Test the component type detection logic"""
# Simulate the component type detection logic from the version command
def detect_component_type(provider_type, adapter_type=None):
if provider_type.startswith("inline::"):
return "inline", provider_type.replace("inline::", "")
elif provider_type.startswith("remote::"):
return "remote", provider_type.replace("remote::", "")
elif adapter_type:
return "remote", adapter_type
else:
return "unknown", provider_type
# Test cases
test_cases = [
("inline::meta-reference", None, "inline", "meta-reference"),
("remote::ollama", None, "remote", "ollama"),
("some-provider", "adapter-name", "remote", "adapter-name"),
("unknown-provider", None, "unknown", "unknown-provider"),
]
for provider_type, adapter_type, expected_type, expected_name in test_cases:
comp_type, comp_name = detect_component_type(provider_type, adapter_type)
assert comp_type == expected_type, f"Expected type {expected_type}, got {comp_type}"
assert comp_name == expected_name, f"Expected name {expected_name}, got {comp_name}"
print("✓ Component type detection logic test passed")
return True
def test_python_version_format():
"""Test that Python version formatting works correctly"""
# Simulate the Python version formatting from the version command
python_version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
# Check format
parts = python_version.split(".")
assert len(parts) == 3, "Python version should have 3 parts"
assert all(part.isdigit() for part in parts), "All parts should be numeric"
print(f"✓ Python version format test passed: {python_version}")
return True
def test_json_output_structure():
"""Test the expected JSON output structure"""
# Expected structure for JSON output
expected_fields = {
"llama_stack_version": str,
"llama_stack_client_version": str,
"python_version": str,
"git_commit": str,
"git_commit_date": str,
"git_branch": str,
"git_tag": str,
"build_timestamp": str,
}
# Test that we can create a valid JSON structure
test_data = {field: "test_value" for field in expected_fields.keys()}
# Should be valid JSON
json_str = json.dumps(test_data, indent=2)
parsed = json.loads(json_str)
# Should have all expected fields
for field in expected_fields.keys():
assert field in parsed, f"Missing field: {field}"
print("✓ JSON output structure test passed")
return True
def run_all_tests():
"""Run all simple unit tests"""
tests = [
test_build_info_file_structure,
test_build_script_exists,
test_version_module_structure,
test_cli_integration,
test_gitignore_entry,
test_component_type_detection_logic,
test_python_version_format,
test_json_output_structure,
]
passed = 0
failed = 0
print("Running simple version command unit tests...")
for test in tests:
try:
if test():
passed += 1
except Exception as e:
print(f"✗ Test {test.__name__} failed: {e}")
failed += 1
print(f"\nResults: {passed} passed, {failed} failed")
return failed == 0
if __name__ == "__main__":
"""Run tests when executed directly"""
success = run_all_tests()
if not success:
sys.exit(1)
else:
print("All simple unit tests passed!")