feat: use XDG directory standards

Signed-off-by: Mustafa Elbehery <melbeher@redhat.com>
This commit is contained in:
Mustafa Elbehery 2025-07-03 18:48:53 +02:00
parent 9736f096f6
commit 407c3e3bad
50 changed files with 5611 additions and 508 deletions

View file

@ -0,0 +1,489 @@
# 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.
# All rights reserved.
#
# This source code is licensed under the terms described in the LICENSE file in
# the root directory of this source tree.
# 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 os
import tempfile
import unittest
from io import StringIO
from pathlib import Path
from unittest.mock import patch
from llama_stack.cli.migrate_xdg import MigrateXDG, migrate_to_xdg
class TestMigrateXDGCLI(unittest.TestCase):
"""Test the MigrateXDG CLI command."""
def setUp(self):
"""Set up test environment."""
self.original_env = {}
for key in ["XDG_CONFIG_HOME", "XDG_DATA_HOME", "XDG_STATE_HOME", "LLAMA_STACK_CONFIG_DIR"]:
self.original_env[key] = os.environ.get(key)
os.environ.pop(key, None)
def tearDown(self):
"""Clean up test environment."""
for key, value in self.original_env.items():
if value is None:
os.environ.pop(key, None)
else:
os.environ[key] = value
def create_parser_with_migrate_cmd(self):
"""Create parser with migrate-xdg command."""
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(dest="command")
migrate_cmd = MigrateXDG.create(subparsers)
return parser, migrate_cmd
def test_migrate_xdg_command_creation(self):
"""Test that MigrateXDG command can be created."""
parser, migrate_cmd = self.create_parser_with_migrate_cmd()
self.assertIsInstance(migrate_cmd, MigrateXDG)
self.assertEqual(migrate_cmd.parser.prog, "llama migrate-xdg")
self.assertEqual(migrate_cmd.parser.description, "Migrate from legacy ~/.llama to XDG-compliant directories")
def test_migrate_xdg_argument_parsing(self):
"""Test argument parsing for migrate-xdg command."""
parser, _ = self.create_parser_with_migrate_cmd()
# Test with dry-run flag
args = parser.parse_args(["migrate-xdg", "--dry-run"])
self.assertEqual(args.command, "migrate-xdg")
self.assertTrue(args.dry_run)
# Test without dry-run flag
args = parser.parse_args(["migrate-xdg"])
self.assertEqual(args.command, "migrate-xdg")
self.assertFalse(args.dry_run)
def test_migrate_xdg_help_text(self):
"""Test help text for migrate-xdg command."""
parser, _ = self.create_parser_with_migrate_cmd()
# Capture help output
with patch("sys.stdout", new_callable=StringIO) as mock_stdout:
with patch("sys.exit"):
try:
parser.parse_args(["migrate-xdg", "--help"])
except SystemExit:
pass
help_text = mock_stdout.getvalue()
self.assertIn("migrate-xdg", help_text)
self.assertIn("XDG-compliant directories", help_text)
self.assertIn("--dry-run", help_text)
def test_migrate_xdg_command_execution_no_legacy(self):
"""Test command execution when no legacy directory exists."""
parser, migrate_cmd = self.create_parser_with_migrate_cmd()
with tempfile.TemporaryDirectory() as temp_dir:
with patch("llama_stack.distribution.utils.xdg_utils.Path.home") as mock_home:
mock_home.return_value = Path(temp_dir)
args = parser.parse_args(["migrate-xdg"])
with patch("builtins.print") as mock_print:
result = migrate_cmd._run_migrate_xdg_cmd(args)
# Should succeed when no migration needed
self.assertEqual(result, 0)
# Should print appropriate message
print_calls = [call[0][0] for call in mock_print.call_args_list]
self.assertTrue(any("No legacy directory found" in call for call in print_calls))
def test_migrate_xdg_command_execution_with_legacy(self):
"""Test command execution when legacy directory exists."""
parser, migrate_cmd = self.create_parser_with_migrate_cmd()
with tempfile.TemporaryDirectory() as temp_dir:
base_dir = Path(temp_dir)
legacy_dir = base_dir / ".llama"
legacy_dir.mkdir()
(legacy_dir / "test_file").touch()
with patch("llama_stack.distribution.utils.xdg_utils.Path.home") as mock_home:
mock_home.return_value = base_dir
args = parser.parse_args(["migrate-xdg"])
with patch("builtins.print") as mock_print:
result = migrate_cmd._run_migrate_xdg_cmd(args)
# Should succeed
self.assertEqual(result, 0)
# Should print migration information
print_calls = [call[0][0] for call in mock_print.call_args_list]
self.assertTrue(any("Found legacy directory" in call for call in print_calls))
def test_migrate_xdg_command_execution_dry_run(self):
"""Test command execution with dry-run flag."""
parser, migrate_cmd = self.create_parser_with_migrate_cmd()
with tempfile.TemporaryDirectory() as temp_dir:
base_dir = Path(temp_dir)
legacy_dir = base_dir / ".llama"
legacy_dir.mkdir()
(legacy_dir / "test_file").touch()
with patch("llama_stack.distribution.utils.xdg_utils.Path.home") as mock_home:
mock_home.return_value = base_dir
args = parser.parse_args(["migrate-xdg", "--dry-run"])
with patch("builtins.print") as mock_print:
result = migrate_cmd._run_migrate_xdg_cmd(args)
# Should succeed
self.assertEqual(result, 0)
# Should print dry-run information
print_calls = [call[0][0] for call in mock_print.call_args_list]
self.assertTrue(any("Dry run mode" in call for call in print_calls))
def test_migrate_xdg_command_execution_error_handling(self):
"""Test command execution with error handling."""
parser, migrate_cmd = self.create_parser_with_migrate_cmd()
args = parser.parse_args(["migrate-xdg"])
# Mock migrate_to_xdg to raise an exception
with patch("llama_stack.cli.migrate_xdg.migrate_to_xdg") as mock_migrate:
mock_migrate.side_effect = Exception("Test error")
with patch("builtins.print") as mock_print:
result = migrate_cmd._run_migrate_xdg_cmd(args)
# Should return error code
self.assertEqual(result, 1)
# Should print error message
print_calls = [call[0][0] for call in mock_print.call_args_list]
self.assertTrue(any("Error during migration" in call for call in print_calls))
def test_migrate_xdg_command_integration(self):
"""Test full integration of migrate-xdg command."""
from llama_stack.cli.llama import LlamaCLIParser
# Create main parser
main_parser = LlamaCLIParser()
# Test that migrate-xdg is in the subcommands
with patch("sys.argv", ["llama", "migrate-xdg", "--help"]):
with patch("sys.stdout", new_callable=StringIO) as mock_stdout:
with patch("sys.exit"):
try:
main_parser.parse_args()
except SystemExit:
pass
help_text = mock_stdout.getvalue()
self.assertIn("migrate-xdg", help_text)
class TestMigrateXDGFunction(unittest.TestCase):
"""Test the migrate_to_xdg function directly."""
def setUp(self):
"""Set up test environment."""
self.original_env = {}
for key in ["XDG_CONFIG_HOME", "XDG_DATA_HOME", "XDG_STATE_HOME", "LLAMA_STACK_CONFIG_DIR"]:
self.original_env[key] = os.environ.get(key)
os.environ.pop(key, None)
def tearDown(self):
"""Clean up test environment."""
for key, value in self.original_env.items():
if value is None:
os.environ.pop(key, None)
else:
os.environ[key] = value
def create_legacy_structure(self, base_dir: Path) -> Path:
"""Create a test legacy directory structure."""
legacy_dir = base_dir / ".llama"
legacy_dir.mkdir()
# Create distributions
(legacy_dir / "distributions").mkdir()
(legacy_dir / "distributions" / "ollama").mkdir()
(legacy_dir / "distributions" / "ollama" / "run.yaml").write_text("version: 2\n")
# Create checkpoints
(legacy_dir / "checkpoints").mkdir()
(legacy_dir / "checkpoints" / "model.bin").write_text("fake model")
# Create providers.d
(legacy_dir / "providers.d").mkdir()
(legacy_dir / "providers.d" / "provider.yaml").write_text("provider: test\n")
# Create runtime
(legacy_dir / "runtime").mkdir()
(legacy_dir / "runtime" / "trace.db").write_text("fake database")
return legacy_dir
def test_migrate_to_xdg_no_legacy_directory(self):
"""Test migrate_to_xdg when no legacy directory exists."""
with tempfile.TemporaryDirectory() as temp_dir:
with patch("llama_stack.distribution.utils.xdg_utils.Path.home") as mock_home:
mock_home.return_value = Path(temp_dir)
result = migrate_to_xdg(dry_run=False)
self.assertTrue(result)
def test_migrate_to_xdg_dry_run(self):
"""Test migrate_to_xdg with dry_run=True."""
with tempfile.TemporaryDirectory() as temp_dir:
base_dir = Path(temp_dir)
legacy_dir = self.create_legacy_structure(base_dir)
with patch("llama_stack.distribution.utils.xdg_utils.Path.home") as mock_home:
mock_home.return_value = base_dir
with patch("builtins.print") as mock_print:
result = migrate_to_xdg(dry_run=True)
self.assertTrue(result)
# Should print dry run information
print_calls = [call[0][0] for call in mock_print.call_args_list]
self.assertTrue(any("Dry run mode" in call for call in print_calls))
# Legacy directory should still exist
self.assertTrue(legacy_dir.exists())
def test_migrate_to_xdg_user_confirms(self):
"""Test migrate_to_xdg when user confirms migration."""
with tempfile.TemporaryDirectory() as temp_dir:
base_dir = Path(temp_dir)
legacy_dir = self.create_legacy_structure(base_dir)
with patch("llama_stack.distribution.utils.xdg_utils.Path.home") as mock_home:
mock_home.return_value = base_dir
with patch("builtins.input") as mock_input:
mock_input.side_effect = ["y", "y"] # Confirm migration and cleanup
result = migrate_to_xdg(dry_run=False)
self.assertTrue(result)
# Legacy directory should be removed
self.assertFalse(legacy_dir.exists())
# XDG directories should be created
self.assertTrue((base_dir / ".config" / "llama-stack").exists())
self.assertTrue((base_dir / ".local" / "share" / "llama-stack").exists())
def test_migrate_to_xdg_user_cancels(self):
"""Test migrate_to_xdg when user cancels migration."""
with tempfile.TemporaryDirectory() as temp_dir:
base_dir = Path(temp_dir)
legacy_dir = self.create_legacy_structure(base_dir)
with patch("llama_stack.distribution.utils.xdg_utils.Path.home") as mock_home:
mock_home.return_value = base_dir
with patch("builtins.input") as mock_input:
mock_input.return_value = "n" # Cancel migration
result = migrate_to_xdg(dry_run=False)
self.assertFalse(result)
# Legacy directory should still exist
self.assertTrue(legacy_dir.exists())
def test_migrate_to_xdg_partial_migration(self):
"""Test migrate_to_xdg with partial migration (some files fail)."""
with tempfile.TemporaryDirectory() as temp_dir:
base_dir = Path(temp_dir)
legacy_dir = self.create_legacy_structure(base_dir)
self.assertFalse(legacy_dir.exists())
with patch("llama_stack.distribution.utils.xdg_utils.Path.home") as mock_home:
mock_home.return_value = base_dir
# Create target directory with conflicting file
config_dir = base_dir / ".config" / "llama-stack"
config_dir.mkdir(parents=True)
(config_dir / "distributions").mkdir()
(config_dir / "distributions" / "existing.yaml").write_text("existing")
with patch("builtins.input") as mock_input:
mock_input.side_effect = ["y", "n"] # Confirm migration, don't cleanup
with patch("builtins.print") as mock_print:
result = migrate_to_xdg(dry_run=False)
self.assertTrue(result)
# Should print warning about conflicts
print_calls = [call[0][0] for call in mock_print.call_args_list]
self.assertTrue(any("Warning: Target already exists" in call for call in print_calls))
def test_migrate_to_xdg_permission_error(self):
"""Test migrate_to_xdg with permission errors."""
with tempfile.TemporaryDirectory() as temp_dir:
base_dir = Path(temp_dir)
legacy_dir = self.create_legacy_structure(base_dir)
self.assertFalse(legacy_dir.exists())
with patch("llama_stack.distribution.utils.xdg_utils.Path.home") as mock_home:
mock_home.return_value = base_dir
# Create readonly target directory
config_dir = base_dir / ".config" / "llama-stack"
config_dir.mkdir(parents=True)
config_dir.chmod(0o444) # Read-only
try:
with patch("builtins.input") as mock_input:
mock_input.side_effect = ["y", "n"] # Confirm migration, don't cleanup
with patch("builtins.print") as mock_print:
result = migrate_to_xdg(dry_run=False)
self.assertFalse(result)
# Should handle permission errors gracefully
print_calls = [call[0][0] for call in mock_print.call_args_list]
# Should contain some error or warning message
self.assertTrue(len(print_calls) > 0)
finally:
# Restore permissions for cleanup
config_dir.chmod(0o755)
def test_migrate_to_xdg_empty_legacy_directory(self):
"""Test migrate_to_xdg with empty legacy directory."""
with tempfile.TemporaryDirectory() as temp_dir:
base_dir = Path(temp_dir)
legacy_dir = base_dir / ".llama"
legacy_dir.mkdir() # Empty directory
with patch("llama_stack.distribution.utils.xdg_utils.Path.home") as mock_home:
mock_home.return_value = base_dir
result = migrate_to_xdg(dry_run=False)
self.assertTrue(result)
def test_migrate_to_xdg_preserves_file_content(self):
"""Test that migrate_to_xdg preserves file content correctly."""
with tempfile.TemporaryDirectory() as temp_dir:
base_dir = Path(temp_dir)
legacy_dir = self.create_legacy_structure(base_dir)
# Add specific content to test
test_content = "test configuration content"
(legacy_dir / "distributions" / "ollama" / "run.yaml").write_text(test_content)
with patch("llama_stack.distribution.utils.xdg_utils.Path.home") as mock_home:
mock_home.return_value = base_dir
with patch("builtins.input") as mock_input:
mock_input.side_effect = ["y", "y"] # Confirm migration and cleanup
result = migrate_to_xdg(dry_run=False)
self.assertTrue(result)
# Check content was preserved
migrated_file = base_dir / ".config" / "llama-stack" / "distributions" / "ollama" / "run.yaml"
self.assertTrue(migrated_file.exists())
self.assertEqual(migrated_file.read_text(), test_content)
def test_migrate_to_xdg_with_symlinks(self):
"""Test migrate_to_xdg with symbolic links."""
with tempfile.TemporaryDirectory() as temp_dir:
base_dir = Path(temp_dir)
legacy_dir = self.create_legacy_structure(base_dir)
# Create symlink
actual_file = legacy_dir / "actual_config.yaml"
actual_file.write_text("actual config")
symlink_file = legacy_dir / "distributions" / "symlinked.yaml"
symlink_file.symlink_to(actual_file)
with patch("llama_stack.distribution.utils.xdg_utils.Path.home") as mock_home:
mock_home.return_value = base_dir
with patch("builtins.input") as mock_input:
mock_input.side_effect = ["y", "y"] # Confirm migration and cleanup
result = migrate_to_xdg(dry_run=False)
self.assertTrue(result)
# Check symlink was preserved
migrated_symlink = base_dir / ".config" / "llama-stack" / "distributions" / "symlinked.yaml"
self.assertTrue(migrated_symlink.exists())
self.assertTrue(migrated_symlink.is_symlink())
def test_migrate_to_xdg_nested_directory_structure(self):
"""Test migrate_to_xdg with nested directory structures."""
with tempfile.TemporaryDirectory() as temp_dir:
base_dir = Path(temp_dir)
legacy_dir = self.create_legacy_structure(base_dir)
# Create nested structure
nested_dir = legacy_dir / "checkpoints" / "org" / "model" / "variant"
nested_dir.mkdir(parents=True)
(nested_dir / "model.bin").write_text("nested model")
with patch("llama_stack.distribution.utils.xdg_utils.Path.home") as mock_home:
mock_home.return_value = base_dir
with patch("builtins.input") as mock_input:
mock_input.side_effect = ["y", "y"] # Confirm migration and cleanup
result = migrate_to_xdg(dry_run=False)
self.assertTrue(result)
# Check nested structure was preserved
migrated_nested = (
base_dir / ".local" / "share" / "llama-stack" / "checkpoints" / "org" / "model" / "variant"
)
self.assertTrue(migrated_nested.exists())
self.assertTrue((migrated_nested / "model.bin").exists())
def test_migrate_to_xdg_user_input_variations(self):
"""Test migrate_to_xdg with various user input variations."""
with tempfile.TemporaryDirectory() as temp_dir:
base_dir = Path(temp_dir)
legacy_dir = self.create_legacy_structure(base_dir)
with patch("llama_stack.distribution.utils.xdg_utils.Path.home") as mock_home:
mock_home.return_value = base_dir
# Test various forms of "yes"
for yes_input in ["y", "Y", "yes", "Yes", "YES"]:
# Recreate legacy directory for each test
if legacy_dir.exists():
import shutil
shutil.rmtree(legacy_dir)
self.create_legacy_structure(base_dir)
with patch("builtins.input") as mock_input:
mock_input.side_effect = [yes_input, "n"] # Confirm migration, don't cleanup
result = migrate_to_xdg(dry_run=False)
self.assertTrue(result, f"Failed with input: {yes_input}")
if __name__ == "__main__":
unittest.main()