mirror of
https://github.com/meta-llama/llama-stack.git
synced 2025-07-27 06:28:50 +00:00
Merge 407c3e3bad
into 632cf9eb72
This commit is contained in:
commit
172d578b20
50 changed files with 5611 additions and 508 deletions
489
tests/unit/cli/test_migrate_xdg.py
Normal file
489
tests/unit/cli/test_migrate_xdg.py
Normal 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()
|
418
tests/unit/test_config_dirs.py
Normal file
418
tests/unit/test_config_dirs.py
Normal file
|
@ -0,0 +1,418 @@
|
|||
# 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 os
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
# Import after we set up environment to avoid module-level imports affecting tests
|
||||
class TestConfigDirs(unittest.TestCase):
|
||||
"""Test the config_dirs module with XDG compliance and backwards compatibility."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test environment."""
|
||||
# Store original environment variables
|
||||
self.original_env = {}
|
||||
self.env_vars = [
|
||||
"XDG_CONFIG_HOME",
|
||||
"XDG_DATA_HOME",
|
||||
"XDG_STATE_HOME",
|
||||
"XDG_CACHE_HOME",
|
||||
"LLAMA_STACK_CONFIG_DIR",
|
||||
"SQLITE_STORE_DIR",
|
||||
"FILES_STORAGE_DIR",
|
||||
]
|
||||
|
||||
for key in self.env_vars:
|
||||
self.original_env[key] = os.environ.get(key)
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up test environment."""
|
||||
# Restore original environment variables
|
||||
for key, value in self.original_env.items():
|
||||
if value is None:
|
||||
os.environ.pop(key, None)
|
||||
else:
|
||||
os.environ[key] = value
|
||||
|
||||
# Clear module cache to ensure fresh imports
|
||||
import sys
|
||||
|
||||
modules_to_clear = ["llama_stack.distribution.utils.config_dirs", "llama_stack.distribution.utils.xdg_utils"]
|
||||
for module in modules_to_clear:
|
||||
if module in sys.modules:
|
||||
del sys.modules[module]
|
||||
|
||||
def clear_env_vars(self):
|
||||
"""Clear all relevant environment variables."""
|
||||
for key in self.env_vars:
|
||||
os.environ.pop(key, None)
|
||||
|
||||
def test_config_dirs_xdg_defaults(self):
|
||||
"""Test config_dirs with XDG default paths."""
|
||||
self.clear_env_vars()
|
||||
|
||||
# Mock that no legacy directory exists
|
||||
with patch("llama_stack.distribution.utils.xdg_utils.Path.home") as mock_home:
|
||||
mock_home.return_value = Path("/home/testuser")
|
||||
with patch("llama_stack.distribution.utils.xdg_utils.Path.exists") as mock_exists:
|
||||
mock_exists.return_value = False
|
||||
|
||||
# Import after setting up mocks
|
||||
from llama_stack.distribution.utils.config_dirs import (
|
||||
DEFAULT_CHECKPOINT_DIR,
|
||||
DISTRIBS_BASE_DIR,
|
||||
EXTERNAL_PROVIDERS_DIR,
|
||||
LLAMA_STACK_CONFIG_DIR,
|
||||
RUNTIME_BASE_DIR,
|
||||
)
|
||||
|
||||
# Verify XDG-compliant paths
|
||||
self.assertEqual(LLAMA_STACK_CONFIG_DIR, Path("/home/testuser/.config/llama-stack"))
|
||||
self.assertEqual(DEFAULT_CHECKPOINT_DIR, Path("/home/testuser/.local/share/llama-stack/checkpoints"))
|
||||
self.assertEqual(RUNTIME_BASE_DIR, Path("/home/testuser/.local/state/llama-stack/runtime"))
|
||||
self.assertEqual(EXTERNAL_PROVIDERS_DIR, Path("/home/testuser/.config/llama-stack/providers.d"))
|
||||
self.assertEqual(DISTRIBS_BASE_DIR, Path("/home/testuser/.config/llama-stack/distributions"))
|
||||
|
||||
def test_config_dirs_custom_xdg_paths(self):
|
||||
"""Test config_dirs with custom XDG paths."""
|
||||
self.clear_env_vars()
|
||||
|
||||
# Set custom XDG paths
|
||||
os.environ["XDG_CONFIG_HOME"] = "/custom/config"
|
||||
os.environ["XDG_DATA_HOME"] = "/custom/data"
|
||||
os.environ["XDG_STATE_HOME"] = "/custom/state"
|
||||
|
||||
# Mock that no legacy directory exists
|
||||
with patch("llama_stack.distribution.utils.xdg_utils.Path.exists") as mock_exists:
|
||||
mock_exists.return_value = False
|
||||
|
||||
from llama_stack.distribution.utils.config_dirs import (
|
||||
DEFAULT_CHECKPOINT_DIR,
|
||||
DISTRIBS_BASE_DIR,
|
||||
EXTERNAL_PROVIDERS_DIR,
|
||||
LLAMA_STACK_CONFIG_DIR,
|
||||
RUNTIME_BASE_DIR,
|
||||
)
|
||||
|
||||
# Verify custom XDG paths are used
|
||||
self.assertEqual(LLAMA_STACK_CONFIG_DIR, Path("/custom/config/llama-stack"))
|
||||
self.assertEqual(DEFAULT_CHECKPOINT_DIR, Path("/custom/data/llama-stack/checkpoints"))
|
||||
self.assertEqual(RUNTIME_BASE_DIR, Path("/custom/state/llama-stack/runtime"))
|
||||
self.assertEqual(EXTERNAL_PROVIDERS_DIR, Path("/custom/config/llama-stack/providers.d"))
|
||||
self.assertEqual(DISTRIBS_BASE_DIR, Path("/custom/config/llama-stack/distributions"))
|
||||
|
||||
def test_config_dirs_legacy_environment_variable(self):
|
||||
"""Test config_dirs with legacy LLAMA_STACK_CONFIG_DIR."""
|
||||
self.clear_env_vars()
|
||||
|
||||
# Set legacy environment variable
|
||||
os.environ["LLAMA_STACK_CONFIG_DIR"] = "/legacy/llama"
|
||||
|
||||
from llama_stack.distribution.utils.config_dirs import (
|
||||
DEFAULT_CHECKPOINT_DIR,
|
||||
DISTRIBS_BASE_DIR,
|
||||
EXTERNAL_PROVIDERS_DIR,
|
||||
LLAMA_STACK_CONFIG_DIR,
|
||||
RUNTIME_BASE_DIR,
|
||||
)
|
||||
|
||||
# All paths should use the legacy base
|
||||
legacy_base = Path("/legacy/llama")
|
||||
self.assertEqual(LLAMA_STACK_CONFIG_DIR, legacy_base)
|
||||
self.assertEqual(DEFAULT_CHECKPOINT_DIR, legacy_base / "checkpoints")
|
||||
self.assertEqual(RUNTIME_BASE_DIR, legacy_base / "runtime")
|
||||
self.assertEqual(EXTERNAL_PROVIDERS_DIR, legacy_base / "providers.d")
|
||||
self.assertEqual(DISTRIBS_BASE_DIR, legacy_base / "distributions")
|
||||
|
||||
def test_config_dirs_legacy_directory_exists(self):
|
||||
"""Test config_dirs when legacy ~/.llama directory exists."""
|
||||
self.clear_env_vars()
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
home_dir = Path(temp_dir)
|
||||
legacy_dir = home_dir / ".llama"
|
||||
legacy_dir.mkdir()
|
||||
(legacy_dir / "test_file").touch() # Add content
|
||||
|
||||
with patch("llama_stack.distribution.utils.xdg_utils.Path.home") as mock_home:
|
||||
mock_home.return_value = home_dir
|
||||
|
||||
from llama_stack.distribution.utils.config_dirs import (
|
||||
DEFAULT_CHECKPOINT_DIR,
|
||||
DISTRIBS_BASE_DIR,
|
||||
EXTERNAL_PROVIDERS_DIR,
|
||||
LLAMA_STACK_CONFIG_DIR,
|
||||
RUNTIME_BASE_DIR,
|
||||
)
|
||||
|
||||
# Should use legacy directory
|
||||
self.assertEqual(LLAMA_STACK_CONFIG_DIR, legacy_dir)
|
||||
self.assertEqual(DEFAULT_CHECKPOINT_DIR, legacy_dir / "checkpoints")
|
||||
self.assertEqual(RUNTIME_BASE_DIR, legacy_dir / "runtime")
|
||||
self.assertEqual(EXTERNAL_PROVIDERS_DIR, legacy_dir / "providers.d")
|
||||
self.assertEqual(DISTRIBS_BASE_DIR, legacy_dir / "distributions")
|
||||
|
||||
def test_config_dirs_precedence_order(self):
|
||||
"""Test precedence order: LLAMA_STACK_CONFIG_DIR > legacy directory > XDG."""
|
||||
self.clear_env_vars()
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
home_dir = Path(temp_dir)
|
||||
legacy_dir = home_dir / ".llama"
|
||||
legacy_dir.mkdir()
|
||||
(legacy_dir / "test_file").touch()
|
||||
|
||||
# Set both legacy env var and XDG vars
|
||||
os.environ["LLAMA_STACK_CONFIG_DIR"] = "/priority/path"
|
||||
os.environ["XDG_CONFIG_HOME"] = "/custom/config"
|
||||
|
||||
with patch("llama_stack.distribution.utils.xdg_utils.Path.home") as mock_home:
|
||||
mock_home.return_value = home_dir
|
||||
|
||||
from llama_stack.distribution.utils.config_dirs import LLAMA_STACK_CONFIG_DIR
|
||||
|
||||
# Legacy env var should take precedence
|
||||
self.assertEqual(LLAMA_STACK_CONFIG_DIR, Path("/priority/path"))
|
||||
|
||||
def test_config_dirs_all_path_types(self):
|
||||
"""Test that all path objects are of correct type and absolute."""
|
||||
self.clear_env_vars()
|
||||
|
||||
from llama_stack.distribution.utils.config_dirs import (
|
||||
DEFAULT_CHECKPOINT_DIR,
|
||||
DISTRIBS_BASE_DIR,
|
||||
EXTERNAL_PROVIDERS_DIR,
|
||||
LLAMA_STACK_CONFIG_DIR,
|
||||
RUNTIME_BASE_DIR,
|
||||
)
|
||||
|
||||
# All should be Path objects
|
||||
paths = [
|
||||
LLAMA_STACK_CONFIG_DIR,
|
||||
DEFAULT_CHECKPOINT_DIR,
|
||||
RUNTIME_BASE_DIR,
|
||||
EXTERNAL_PROVIDERS_DIR,
|
||||
DISTRIBS_BASE_DIR,
|
||||
]
|
||||
|
||||
for path in paths:
|
||||
self.assertIsInstance(path, Path, f"Path {path} should be Path object")
|
||||
self.assertTrue(path.is_absolute(), f"Path {path} should be absolute")
|
||||
|
||||
def test_config_dirs_directory_relationships(self):
|
||||
"""Test relationships between different directory paths."""
|
||||
self.clear_env_vars()
|
||||
|
||||
from llama_stack.distribution.utils.config_dirs import (
|
||||
DISTRIBS_BASE_DIR,
|
||||
EXTERNAL_PROVIDERS_DIR,
|
||||
LLAMA_STACK_CONFIG_DIR,
|
||||
)
|
||||
|
||||
# Test parent-child relationships
|
||||
self.assertEqual(EXTERNAL_PROVIDERS_DIR.parent, LLAMA_STACK_CONFIG_DIR)
|
||||
self.assertEqual(DISTRIBS_BASE_DIR.parent, LLAMA_STACK_CONFIG_DIR)
|
||||
|
||||
# Test expected subdirectory names
|
||||
self.assertEqual(EXTERNAL_PROVIDERS_DIR.name, "providers.d")
|
||||
self.assertEqual(DISTRIBS_BASE_DIR.name, "distributions")
|
||||
|
||||
def test_config_dirs_environment_isolation(self):
|
||||
"""Test that config_dirs is properly isolated between tests."""
|
||||
self.clear_env_vars()
|
||||
|
||||
# First import with one set of environment variables
|
||||
os.environ["LLAMA_STACK_CONFIG_DIR"] = "/first/path"
|
||||
|
||||
# Clear module cache
|
||||
import sys
|
||||
|
||||
if "llama_stack.distribution.utils.config_dirs" in sys.modules:
|
||||
del sys.modules["llama_stack.distribution.utils.config_dirs"]
|
||||
|
||||
from llama_stack.distribution.utils.config_dirs import LLAMA_STACK_CONFIG_DIR as FIRST_CONFIG
|
||||
|
||||
# Change environment and re-import
|
||||
os.environ["LLAMA_STACK_CONFIG_DIR"] = "/second/path"
|
||||
|
||||
# Clear module cache again
|
||||
if "llama_stack.distribution.utils.config_dirs" in sys.modules:
|
||||
del sys.modules["llama_stack.distribution.utils.config_dirs"]
|
||||
|
||||
from llama_stack.distribution.utils.config_dirs import LLAMA_STACK_CONFIG_DIR as SECOND_CONFIG
|
||||
|
||||
# Should get different paths
|
||||
self.assertEqual(FIRST_CONFIG, Path("/first/path"))
|
||||
self.assertEqual(SECOND_CONFIG, Path("/second/path"))
|
||||
|
||||
def test_config_dirs_with_tilde_expansion(self):
|
||||
"""Test config_dirs with tilde in paths."""
|
||||
self.clear_env_vars()
|
||||
|
||||
os.environ["LLAMA_STACK_CONFIG_DIR"] = "~/custom_llama"
|
||||
|
||||
from llama_stack.distribution.utils.config_dirs import LLAMA_STACK_CONFIG_DIR
|
||||
|
||||
# Should expand tilde
|
||||
expected = Path.home() / "custom_llama"
|
||||
self.assertEqual(LLAMA_STACK_CONFIG_DIR, expected)
|
||||
|
||||
def test_config_dirs_empty_environment_variables(self):
|
||||
"""Test config_dirs with empty environment variables."""
|
||||
self.clear_env_vars()
|
||||
|
||||
# Set empty values
|
||||
os.environ["XDG_CONFIG_HOME"] = ""
|
||||
os.environ["XDG_DATA_HOME"] = ""
|
||||
|
||||
# Mock no legacy directory
|
||||
with patch("llama_stack.distribution.utils.xdg_utils.Path.home") as mock_home:
|
||||
mock_home.return_value = Path("/home/testuser")
|
||||
with patch("llama_stack.distribution.utils.xdg_utils.Path.exists") as mock_exists:
|
||||
mock_exists.return_value = False
|
||||
|
||||
from llama_stack.distribution.utils.config_dirs import (
|
||||
DEFAULT_CHECKPOINT_DIR,
|
||||
LLAMA_STACK_CONFIG_DIR,
|
||||
)
|
||||
|
||||
# Should fall back to defaults
|
||||
self.assertEqual(LLAMA_STACK_CONFIG_DIR, Path("/home/testuser/.config/llama-stack"))
|
||||
self.assertEqual(DEFAULT_CHECKPOINT_DIR, Path("/home/testuser/.local/share/llama-stack/checkpoints"))
|
||||
|
||||
def test_config_dirs_relative_paths(self):
|
||||
"""Test config_dirs with relative paths in environment variables."""
|
||||
self.clear_env_vars()
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
os.chdir(temp_dir)
|
||||
|
||||
# Use relative path
|
||||
os.environ["LLAMA_STACK_CONFIG_DIR"] = "relative/config"
|
||||
|
||||
from llama_stack.distribution.utils.config_dirs import LLAMA_STACK_CONFIG_DIR
|
||||
|
||||
# Should be resolved to absolute path
|
||||
self.assertTrue(LLAMA_STACK_CONFIG_DIR.is_absolute())
|
||||
self.assertTrue(str(LLAMA_STACK_CONFIG_DIR).endswith("relative/config"))
|
||||
|
||||
def test_config_dirs_with_spaces_in_paths(self):
|
||||
"""Test config_dirs with spaces in directory paths."""
|
||||
self.clear_env_vars()
|
||||
|
||||
path_with_spaces = "/path with spaces/llama config"
|
||||
os.environ["LLAMA_STACK_CONFIG_DIR"] = path_with_spaces
|
||||
|
||||
from llama_stack.distribution.utils.config_dirs import LLAMA_STACK_CONFIG_DIR
|
||||
|
||||
self.assertEqual(LLAMA_STACK_CONFIG_DIR, Path(path_with_spaces))
|
||||
|
||||
def test_config_dirs_unicode_paths(self):
|
||||
"""Test config_dirs with unicode characters in paths."""
|
||||
self.clear_env_vars()
|
||||
|
||||
unicode_path = "/配置/llama-stack"
|
||||
os.environ["LLAMA_STACK_CONFIG_DIR"] = unicode_path
|
||||
|
||||
from llama_stack.distribution.utils.config_dirs import LLAMA_STACK_CONFIG_DIR
|
||||
|
||||
self.assertEqual(LLAMA_STACK_CONFIG_DIR, Path(unicode_path))
|
||||
|
||||
def test_config_dirs_compatibility_import(self):
|
||||
"""Test that config_dirs can be imported without errors in various scenarios."""
|
||||
self.clear_env_vars()
|
||||
|
||||
# Test import with no environment variables
|
||||
try:
|
||||
# If we get here without exception, the import succeeded
|
||||
self.assertTrue(True)
|
||||
except Exception as e:
|
||||
self.fail(f"Import failed: {e}")
|
||||
|
||||
def test_config_dirs_multiple_imports(self):
|
||||
"""Test that multiple imports of config_dirs return consistent results."""
|
||||
self.clear_env_vars()
|
||||
|
||||
os.environ["LLAMA_STACK_CONFIG_DIR"] = "/consistent/path"
|
||||
|
||||
# First import
|
||||
from llama_stack.distribution.utils.config_dirs import LLAMA_STACK_CONFIG_DIR as FIRST_IMPORT
|
||||
|
||||
# Second import (should get cached result)
|
||||
from llama_stack.distribution.utils.config_dirs import LLAMA_STACK_CONFIG_DIR as SECOND_IMPORT
|
||||
|
||||
self.assertEqual(FIRST_IMPORT, SECOND_IMPORT)
|
||||
self.assertIs(FIRST_IMPORT, SECOND_IMPORT) # Should be the same object
|
||||
|
||||
|
||||
class TestConfigDirsIntegration(unittest.TestCase):
|
||||
"""Integration tests for config_dirs with other modules."""
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
|
||||
# Clear module cache
|
||||
import sys
|
||||
|
||||
modules_to_clear = ["llama_stack.distribution.utils.config_dirs", "llama_stack.distribution.utils.xdg_utils"]
|
||||
for module in modules_to_clear:
|
||||
if module in sys.modules:
|
||||
del sys.modules[module]
|
||||
|
||||
def test_config_dirs_with_model_utils(self):
|
||||
"""Test that config_dirs works correctly with model_utils."""
|
||||
for key in self.original_env:
|
||||
os.environ.pop(key, None)
|
||||
|
||||
from llama_stack.distribution.utils.config_dirs import DEFAULT_CHECKPOINT_DIR
|
||||
from llama_stack.distribution.utils.model_utils import model_local_dir
|
||||
|
||||
# Test that model_local_dir uses the correct base directory
|
||||
model_descriptor = "meta-llama/Llama-3.2-1B-Instruct"
|
||||
expected_path = str(DEFAULT_CHECKPOINT_DIR / model_descriptor.replace(":", "-"))
|
||||
actual_path = model_local_dir(model_descriptor)
|
||||
|
||||
self.assertEqual(actual_path, expected_path)
|
||||
|
||||
def test_config_dirs_consistency_across_modules(self):
|
||||
"""Test that all modules use consistent directory paths."""
|
||||
for key in self.original_env:
|
||||
os.environ.pop(key, None)
|
||||
|
||||
from llama_stack.distribution.utils.config_dirs import (
|
||||
DEFAULT_CHECKPOINT_DIR,
|
||||
LLAMA_STACK_CONFIG_DIR,
|
||||
RUNTIME_BASE_DIR,
|
||||
)
|
||||
from llama_stack.distribution.utils.xdg_utils import (
|
||||
get_llama_stack_config_dir,
|
||||
get_llama_stack_data_dir,
|
||||
get_llama_stack_state_dir,
|
||||
)
|
||||
|
||||
# Paths should be consistent between modules
|
||||
self.assertEqual(LLAMA_STACK_CONFIG_DIR, get_llama_stack_config_dir())
|
||||
self.assertEqual(DEFAULT_CHECKPOINT_DIR.parent, get_llama_stack_data_dir())
|
||||
self.assertEqual(RUNTIME_BASE_DIR.parent, get_llama_stack_state_dir())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
422
tests/unit/test_template_xdg_paths.py
Normal file
422
tests/unit/test_template_xdg_paths.py
Normal file
|
@ -0,0 +1,422 @@
|
|||
# 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 os
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import yaml
|
||||
|
||||
# Template imports will be tested through file system access
|
||||
|
||||
|
||||
class TestTemplateXDGPaths(unittest.TestCase):
|
||||
"""Test that templates use XDG-compliant paths correctly."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test environment."""
|
||||
self.original_env = {}
|
||||
self.env_vars = [
|
||||
"XDG_CONFIG_HOME",
|
||||
"XDG_DATA_HOME",
|
||||
"XDG_STATE_HOME",
|
||||
"XDG_CACHE_HOME",
|
||||
"LLAMA_STACK_CONFIG_DIR",
|
||||
"SQLITE_STORE_DIR",
|
||||
"FILES_STORAGE_DIR",
|
||||
]
|
||||
|
||||
for key in self.env_vars:
|
||||
self.original_env[key] = os.environ.get(key)
|
||||
|
||||
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 clear_env_vars(self):
|
||||
"""Clear all relevant environment variables."""
|
||||
for key in self.env_vars:
|
||||
os.environ.pop(key, None)
|
||||
|
||||
def test_ollama_template_run_yaml_xdg_paths(self):
|
||||
"""Test that ollama template's run.yaml uses XDG environment variables."""
|
||||
template_path = Path(__file__).parent.parent.parent / "llama_stack" / "templates" / "ollama" / "run.yaml"
|
||||
|
||||
if not template_path.exists():
|
||||
self.skipTest("Ollama template not found")
|
||||
|
||||
content = template_path.read_text()
|
||||
|
||||
# Check for XDG-compliant environment variable references
|
||||
self.assertIn("${env.XDG_STATE_HOME:-~/.local/state}", content)
|
||||
self.assertIn("${env.XDG_DATA_HOME:-~/.local/share}", content)
|
||||
|
||||
# Check that paths use llama-stack directory
|
||||
self.assertIn("llama-stack", content)
|
||||
|
||||
# Check specific path patterns
|
||||
self.assertIn("${env.XDG_STATE_HOME:-~/.local/state}/llama-stack/distributions/ollama", content)
|
||||
self.assertIn("${env.XDG_DATA_HOME:-~/.local/share}/llama-stack/distributions/ollama", content)
|
||||
|
||||
def test_ollama_template_run_yaml_parsing(self):
|
||||
"""Test that ollama template's run.yaml can be parsed correctly."""
|
||||
template_path = Path(__file__).parent.parent.parent / "llama_stack" / "templates" / "ollama" / "run.yaml"
|
||||
|
||||
if not template_path.exists():
|
||||
self.skipTest("Ollama template not found")
|
||||
|
||||
content = template_path.read_text()
|
||||
|
||||
# Replace environment variables with test values for parsing
|
||||
test_content = (
|
||||
content.replace("${env.XDG_STATE_HOME:-~/.local/state}", "/test/state")
|
||||
.replace("${env.XDG_DATA_HOME:-~/.local/share}", "/test/data")
|
||||
.replace(
|
||||
"${env.SQLITE_STORE_DIR:=${env.XDG_STATE_HOME:-~/.local/state}/llama-stack/distributions/ollama}",
|
||||
"/test/state/llama-stack/distributions/ollama",
|
||||
)
|
||||
)
|
||||
|
||||
# Should be valid YAML
|
||||
try:
|
||||
yaml.safe_load(test_content)
|
||||
except yaml.YAMLError as e:
|
||||
self.fail(f"Template YAML is invalid: {e}")
|
||||
|
||||
def test_template_environment_variable_expansion(self):
|
||||
"""Test environment variable expansion in templates."""
|
||||
self.clear_env_vars()
|
||||
|
||||
# Set XDG variables
|
||||
os.environ["XDG_STATE_HOME"] = "/custom/state"
|
||||
os.environ["XDG_DATA_HOME"] = "/custom/data"
|
||||
|
||||
# Test pattern that should expand
|
||||
pattern = "${env.XDG_STATE_HOME:-~/.local/state}/llama-stack/test"
|
||||
expected = "/custom/state/llama-stack/test"
|
||||
|
||||
# Mock environment variable expansion (this would normally be done by the shell)
|
||||
expanded = pattern.replace("${env.XDG_STATE_HOME:-~/.local/state}", os.environ["XDG_STATE_HOME"])
|
||||
self.assertEqual(expanded, expected)
|
||||
|
||||
def test_template_fallback_values(self):
|
||||
"""Test that templates have correct fallback values."""
|
||||
self.clear_env_vars()
|
||||
|
||||
# Test fallback pattern
|
||||
pattern = "${env.XDG_STATE_HOME:-~/.local/state}/llama-stack/test"
|
||||
|
||||
# When environment variable is not set, should use fallback
|
||||
if "XDG_STATE_HOME" not in os.environ:
|
||||
# This is what the shell would do
|
||||
expanded = pattern.replace("${env.XDG_STATE_HOME:-~/.local/state}", "~/.local/state")
|
||||
expected = "~/.local/state/llama-stack/test"
|
||||
self.assertEqual(expanded, expected)
|
||||
|
||||
def test_ollama_template_python_config_xdg(self):
|
||||
"""Test that ollama template's Python config uses XDG-compliant paths."""
|
||||
template_path = Path(__file__).parent.parent.parent / "llama_stack" / "templates" / "ollama" / "ollama.py"
|
||||
|
||||
if not template_path.exists():
|
||||
self.skipTest("Ollama template Python file not found")
|
||||
|
||||
content = template_path.read_text()
|
||||
|
||||
# Check for XDG-compliant environment variable references
|
||||
self.assertIn("${env.XDG_STATE_HOME:-~/.local/state}", content)
|
||||
self.assertIn("${env.XDG_DATA_HOME:-~/.local/share}", content)
|
||||
|
||||
# Check that paths use llama-stack directory
|
||||
self.assertIn("llama-stack", content)
|
||||
|
||||
def test_template_path_consistency(self):
|
||||
"""Test that template paths are consistent across different files."""
|
||||
ollama_yaml_path = Path(__file__).parent.parent.parent / "llama_stack" / "templates" / "ollama" / "run.yaml"
|
||||
ollama_py_path = Path(__file__).parent.parent.parent / "llama_stack" / "templates" / "ollama" / "ollama.py"
|
||||
|
||||
if not ollama_yaml_path.exists() or not ollama_py_path.exists():
|
||||
self.skipTest("Ollama template files not found")
|
||||
|
||||
yaml_content = ollama_yaml_path.read_text()
|
||||
py_content = ollama_py_path.read_text()
|
||||
|
||||
# Both should use the same XDG environment variable patterns
|
||||
xdg_patterns = ["${env.XDG_STATE_HOME:-~/.local/state}", "${env.XDG_DATA_HOME:-~/.local/share}", "llama-stack"]
|
||||
|
||||
for pattern in xdg_patterns:
|
||||
self.assertIn(pattern, yaml_content, f"Pattern {pattern} not found in YAML")
|
||||
self.assertIn(pattern, py_content, f"Pattern {pattern} not found in Python")
|
||||
|
||||
def test_template_no_hardcoded_legacy_paths(self):
|
||||
"""Test that templates don't contain hardcoded legacy paths."""
|
||||
template_dir = Path(__file__).parent.parent.parent / "llama_stack" / "templates"
|
||||
|
||||
if not template_dir.exists():
|
||||
self.skipTest("Templates directory not found")
|
||||
|
||||
# Check various template files
|
||||
for template_path in template_dir.rglob("*.yaml"):
|
||||
content = template_path.read_text()
|
||||
|
||||
# Should not contain hardcoded ~/.llama paths
|
||||
self.assertNotIn("~/.llama", content, f"Found hardcoded ~/.llama in {template_path}")
|
||||
|
||||
# Should not contain hardcoded /tmp paths for persistent data
|
||||
if "db_path" in content or "storage_dir" in content:
|
||||
self.assertNotIn("/tmp", content, f"Found hardcoded /tmp in {template_path}")
|
||||
|
||||
def test_template_environment_variable_format(self):
|
||||
"""Test that templates use correct environment variable format."""
|
||||
template_dir = Path(__file__).parent.parent.parent / "llama_stack" / "templates"
|
||||
|
||||
if not template_dir.exists():
|
||||
self.skipTest("Templates directory not found")
|
||||
|
||||
# Pattern for XDG environment variables with fallbacks
|
||||
xdg_patterns = [
|
||||
"${env.XDG_CONFIG_HOME:-~/.config}",
|
||||
"${env.XDG_DATA_HOME:-~/.local/share}",
|
||||
"${env.XDG_STATE_HOME:-~/.local/state}",
|
||||
"${env.XDG_CACHE_HOME:-~/.cache}",
|
||||
]
|
||||
|
||||
for template_path in template_dir.rglob("*.yaml"):
|
||||
content = template_path.read_text()
|
||||
|
||||
# If XDG variables are used, they should have proper fallbacks
|
||||
for pattern in xdg_patterns:
|
||||
base_var = pattern.split(":-")[0] + "}"
|
||||
if base_var in content:
|
||||
self.assertIn(pattern, content, f"XDG variable without fallback in {template_path}")
|
||||
|
||||
def test_template_sqlite_store_dir_xdg(self):
|
||||
"""Test that SQLITE_STORE_DIR uses XDG-compliant fallback."""
|
||||
template_dir = Path(__file__).parent.parent.parent / "llama_stack" / "templates"
|
||||
|
||||
if not template_dir.exists():
|
||||
self.skipTest("Templates directory not found")
|
||||
|
||||
for template_path in template_dir.rglob("*.yaml"):
|
||||
content = template_path.read_text()
|
||||
|
||||
if "SQLITE_STORE_DIR" in content:
|
||||
# Should use XDG fallback pattern
|
||||
self.assertIn("${env.XDG_STATE_HOME:-~/.local/state}", content)
|
||||
self.assertIn("llama-stack", content)
|
||||
|
||||
def test_template_files_storage_dir_xdg(self):
|
||||
"""Test that FILES_STORAGE_DIR uses XDG-compliant fallback."""
|
||||
template_dir = Path(__file__).parent.parent.parent / "llama_stack" / "templates"
|
||||
|
||||
if not template_dir.exists():
|
||||
self.skipTest("Templates directory not found")
|
||||
|
||||
for template_path in template_dir.rglob("*.yaml"):
|
||||
content = template_path.read_text()
|
||||
|
||||
if "FILES_STORAGE_DIR" in content:
|
||||
# Should use XDG fallback pattern
|
||||
self.assertIn("${env.XDG_DATA_HOME:-~/.local/share}", content)
|
||||
self.assertIn("llama-stack", content)
|
||||
|
||||
|
||||
class TestTemplateCodeGeneration(unittest.TestCase):
|
||||
"""Test template code generation with XDG paths."""
|
||||
|
||||
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)
|
||||
|
||||
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 test_provider_codegen_xdg_paths(self):
|
||||
"""Test that provider code generation uses XDG-compliant paths."""
|
||||
codegen_path = Path(__file__).parent.parent.parent / "scripts" / "provider_codegen.py"
|
||||
|
||||
if not codegen_path.exists():
|
||||
self.skipTest("Provider codegen script not found")
|
||||
|
||||
content = codegen_path.read_text()
|
||||
|
||||
# Should use XDG-compliant path in documentation
|
||||
self.assertIn("${env.XDG_DATA_HOME:-~/.local/share}/llama-stack", content)
|
||||
|
||||
# Should not use hardcoded ~/.llama paths
|
||||
self.assertNotIn("~/.llama/dummy", content)
|
||||
|
||||
def test_template_sample_config_paths(self):
|
||||
"""Test that template sample configs use XDG-compliant paths."""
|
||||
# This test checks that when templates generate sample configs,
|
||||
# they use XDG-compliant paths
|
||||
|
||||
# Mock a template that generates sample config
|
||||
with patch("llama_stack.templates.template.Template") as mock_template:
|
||||
mock_instance = MagicMock()
|
||||
mock_template.return_value = mock_instance
|
||||
|
||||
# Mock sample config generation
|
||||
def mock_sample_config(distro_dir):
|
||||
# Should use XDG-compliant path structure
|
||||
self.assertIn("llama-stack", distro_dir)
|
||||
return {"config": "test"}
|
||||
|
||||
mock_instance.sample_run_config = mock_sample_config
|
||||
|
||||
# Test sample config generation
|
||||
template = mock_template()
|
||||
template.sample_run_config("${env.XDG_DATA_HOME:-~/.local/share}/llama-stack/test")
|
||||
|
||||
def test_template_path_substitution(self):
|
||||
"""Test that template path substitution works correctly."""
|
||||
# Test path substitution in template generation
|
||||
|
||||
original_path = "~/.llama/distributions/test"
|
||||
|
||||
# Should be converted to XDG-compliant path
|
||||
xdg_path = original_path.replace("~/.llama", "${env.XDG_DATA_HOME:-~/.local/share}/llama-stack")
|
||||
expected = "${env.XDG_DATA_HOME:-~/.local/share}/llama-stack/distributions/test"
|
||||
|
||||
self.assertEqual(xdg_path, expected)
|
||||
|
||||
def test_template_environment_variable_precedence(self):
|
||||
"""Test environment variable precedence in templates."""
|
||||
# Test that custom XDG variables take precedence over defaults
|
||||
|
||||
test_cases = [
|
||||
{
|
||||
"env": {"XDG_STATE_HOME": "/custom/state"},
|
||||
"pattern": "${env.XDG_STATE_HOME:-~/.local/state}/llama-stack/test",
|
||||
"expected": "/custom/state/llama-stack/test",
|
||||
},
|
||||
{
|
||||
"env": {}, # No XDG variable set
|
||||
"pattern": "${env.XDG_STATE_HOME:-~/.local/state}/llama-stack/test",
|
||||
"expected": "~/.local/state/llama-stack/test",
|
||||
},
|
||||
]
|
||||
|
||||
for case in test_cases:
|
||||
# Clear environment
|
||||
for key in ["XDG_STATE_HOME", "XDG_DATA_HOME", "XDG_CONFIG_HOME"]:
|
||||
os.environ.pop(key, None)
|
||||
|
||||
# Set test environment
|
||||
for key, value in case["env"].items():
|
||||
os.environ[key] = value
|
||||
|
||||
# Simulate shell variable expansion
|
||||
pattern = case["pattern"]
|
||||
for key, value in case["env"].items():
|
||||
var_pattern = f"${{env.{key}:-"
|
||||
if var_pattern in pattern:
|
||||
# Replace with actual value
|
||||
pattern = pattern.replace(f"${{env.{key}:-~/.local/state}}", value)
|
||||
|
||||
# If no replacement happened, use fallback
|
||||
if "${env.XDG_STATE_HOME:-~/.local/state}" in pattern:
|
||||
pattern = pattern.replace("${env.XDG_STATE_HOME:-~/.local/state}", "~/.local/state")
|
||||
|
||||
self.assertEqual(pattern, case["expected"])
|
||||
|
||||
|
||||
class TestTemplateIntegration(unittest.TestCase):
|
||||
"""Integration tests for templates with XDG compliance."""
|
||||
|
||||
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)
|
||||
|
||||
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 test_template_with_xdg_environment(self):
|
||||
"""Test template behavior with XDG environment variables set."""
|
||||
# Clear environment
|
||||
for key in self.original_env:
|
||||
os.environ.pop(key, None)
|
||||
|
||||
# Set custom XDG variables
|
||||
os.environ["XDG_CONFIG_HOME"] = "/custom/config"
|
||||
os.environ["XDG_DATA_HOME"] = "/custom/data"
|
||||
os.environ["XDG_STATE_HOME"] = "/custom/state"
|
||||
|
||||
# Test that template paths would resolve correctly
|
||||
# (This is a conceptual test since actual shell expansion happens at runtime)
|
||||
|
||||
template_pattern = "${env.XDG_STATE_HOME:-~/.local/state}/llama-stack/test"
|
||||
|
||||
# In a real shell, this would expand to:
|
||||
|
||||
# Verify the pattern structure is correct
|
||||
self.assertIn("XDG_STATE_HOME", template_pattern)
|
||||
self.assertIn("llama-stack", template_pattern)
|
||||
self.assertIn("~/.local/state", template_pattern) # fallback
|
||||
|
||||
def test_template_with_no_xdg_environment(self):
|
||||
"""Test template behavior with no XDG environment variables."""
|
||||
# Clear all XDG environment variables
|
||||
for key in ["XDG_CONFIG_HOME", "XDG_DATA_HOME", "XDG_STATE_HOME", "XDG_CACHE_HOME"]:
|
||||
os.environ.pop(key, None)
|
||||
|
||||
# Test that templates would use fallback values
|
||||
template_pattern = "${env.XDG_STATE_HOME:-~/.local/state}/llama-stack/test"
|
||||
|
||||
# In a real shell with no XDG_STATE_HOME, this would expand to:
|
||||
|
||||
# Verify the pattern structure includes fallback
|
||||
self.assertIn(":-~/.local/state", template_pattern)
|
||||
|
||||
def test_template_consistency_across_providers(self):
|
||||
"""Test that all template providers use consistent XDG patterns."""
|
||||
templates_dir = Path(__file__).parent.parent.parent / "llama_stack" / "templates"
|
||||
|
||||
if not templates_dir.exists():
|
||||
self.skipTest("Templates directory not found")
|
||||
|
||||
# Expected XDG patterns that should be consistent across templates
|
||||
|
||||
# Check a few different provider templates
|
||||
provider_templates = []
|
||||
for provider_dir in templates_dir.iterdir():
|
||||
if provider_dir.is_dir() and not provider_dir.name.startswith("."):
|
||||
run_yaml = provider_dir / "run.yaml"
|
||||
if run_yaml.exists():
|
||||
provider_templates.append(run_yaml)
|
||||
|
||||
if not provider_templates:
|
||||
self.skipTest("No provider templates found")
|
||||
|
||||
# Check that templates use consistent patterns
|
||||
for template_path in provider_templates[:3]: # Check first 3 templates
|
||||
content = template_path.read_text()
|
||||
|
||||
# Should use llama-stack in paths
|
||||
if any(xdg_var in content for xdg_var in ["XDG_CONFIG_HOME", "XDG_DATA_HOME", "XDG_STATE_HOME"]):
|
||||
self.assertIn("llama-stack", content, f"Template {template_path} uses XDG but not llama-stack")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
610
tests/unit/test_xdg_compliance.py
Normal file
610
tests/unit/test_xdg_compliance.py
Normal file
|
@ -0,0 +1,610 @@
|
|||
# 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 os
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from llama_stack.distribution.utils.xdg_utils import (
|
||||
ensure_directory_exists,
|
||||
get_llama_stack_cache_dir,
|
||||
get_llama_stack_config_dir,
|
||||
get_llama_stack_data_dir,
|
||||
get_llama_stack_state_dir,
|
||||
get_xdg_cache_home,
|
||||
get_xdg_compliant_path,
|
||||
get_xdg_config_home,
|
||||
get_xdg_data_home,
|
||||
get_xdg_state_home,
|
||||
migrate_legacy_directory,
|
||||
)
|
||||
|
||||
|
||||
class TestXDGCompliance(unittest.TestCase):
|
||||
"""Comprehensive test suite for XDG Base Directory Specification compliance."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test environment."""
|
||||
# Store original environment variables
|
||||
self.original_env = {}
|
||||
self.xdg_vars = ["XDG_CONFIG_HOME", "XDG_DATA_HOME", "XDG_CACHE_HOME", "XDG_STATE_HOME"]
|
||||
self.llama_vars = ["LLAMA_STACK_CONFIG_DIR", "SQLITE_STORE_DIR", "FILES_STORAGE_DIR"]
|
||||
|
||||
for key in self.xdg_vars + self.llama_vars:
|
||||
self.original_env[key] = os.environ.get(key)
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up test environment."""
|
||||
# Restore original environment variables
|
||||
for key, value in self.original_env.items():
|
||||
if value is None:
|
||||
os.environ.pop(key, None)
|
||||
else:
|
||||
os.environ[key] = value
|
||||
|
||||
def clear_env_vars(self, vars_to_clear=None):
|
||||
"""Helper to clear environment variables."""
|
||||
if vars_to_clear is None:
|
||||
vars_to_clear = self.xdg_vars + self.llama_vars
|
||||
|
||||
for key in vars_to_clear:
|
||||
os.environ.pop(key, None)
|
||||
|
||||
def test_xdg_defaults(self):
|
||||
"""Test that XDG directories use correct defaults when no env vars are set."""
|
||||
self.clear_env_vars()
|
||||
home = Path.home()
|
||||
|
||||
self.assertEqual(get_xdg_config_home(), home / ".config")
|
||||
self.assertEqual(get_xdg_data_home(), home / ".local" / "share")
|
||||
self.assertEqual(get_xdg_cache_home(), home / ".cache")
|
||||
self.assertEqual(get_xdg_state_home(), home / ".local" / "state")
|
||||
|
||||
def test_xdg_custom_paths(self):
|
||||
"""Test that custom XDG paths are respected."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
temp_path = Path(temp_dir)
|
||||
|
||||
os.environ["XDG_CONFIG_HOME"] = str(temp_path / "config")
|
||||
os.environ["XDG_DATA_HOME"] = str(temp_path / "data")
|
||||
os.environ["XDG_CACHE_HOME"] = str(temp_path / "cache")
|
||||
os.environ["XDG_STATE_HOME"] = str(temp_path / "state")
|
||||
|
||||
self.assertEqual(get_xdg_config_home(), temp_path / "config")
|
||||
self.assertEqual(get_xdg_data_home(), temp_path / "data")
|
||||
self.assertEqual(get_xdg_cache_home(), temp_path / "cache")
|
||||
self.assertEqual(get_xdg_state_home(), temp_path / "state")
|
||||
|
||||
def test_xdg_paths_with_tilde(self):
|
||||
"""Test XDG paths that use tilde expansion."""
|
||||
os.environ["XDG_CONFIG_HOME"] = "~/custom_config"
|
||||
os.environ["XDG_DATA_HOME"] = "~/custom_data"
|
||||
|
||||
home = Path.home()
|
||||
self.assertEqual(get_xdg_config_home(), home / "custom_config")
|
||||
self.assertEqual(get_xdg_data_home(), home / "custom_data")
|
||||
|
||||
def test_xdg_paths_relative(self):
|
||||
"""Test XDG paths with relative paths get resolved."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
os.chdir(temp_dir)
|
||||
os.environ["XDG_CONFIG_HOME"] = "relative_config"
|
||||
|
||||
# Should resolve relative to current directory
|
||||
result = get_xdg_config_home()
|
||||
self.assertTrue(result.is_absolute())
|
||||
self.assertTrue(str(result).endswith("relative_config"))
|
||||
|
||||
def test_llama_stack_directories_new_installation(self):
|
||||
"""Test llama-stack directories for new installations (no legacy directory)."""
|
||||
self.clear_env_vars()
|
||||
home = Path.home()
|
||||
|
||||
# Mock that ~/.llama doesn't exist
|
||||
with patch("llama_stack.distribution.utils.xdg_utils.Path.home") as mock_home:
|
||||
mock_home.return_value = home
|
||||
with patch("llama_stack.distribution.utils.xdg_utils.Path.exists") as mock_exists:
|
||||
mock_exists.return_value = False
|
||||
|
||||
self.assertEqual(get_llama_stack_config_dir(), home / ".config" / "llama-stack")
|
||||
self.assertEqual(get_llama_stack_data_dir(), home / ".local" / "share" / "llama-stack")
|
||||
self.assertEqual(get_llama_stack_state_dir(), home / ".local" / "state" / "llama-stack")
|
||||
self.assertEqual(get_llama_stack_cache_dir(), home / ".cache" / "llama-stack")
|
||||
|
||||
def test_llama_stack_directories_with_custom_xdg(self):
|
||||
"""Test llama-stack directories with custom XDG paths."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
temp_path = Path(temp_dir)
|
||||
|
||||
os.environ["XDG_CONFIG_HOME"] = str(temp_path / "config")
|
||||
os.environ["XDG_DATA_HOME"] = str(temp_path / "data")
|
||||
os.environ["XDG_STATE_HOME"] = str(temp_path / "state")
|
||||
os.environ["XDG_CACHE_HOME"] = str(temp_path / "cache")
|
||||
|
||||
# Mock that ~/.llama doesn't exist
|
||||
with patch("llama_stack.distribution.utils.xdg_utils.Path.exists") as mock_exists:
|
||||
mock_exists.return_value = False
|
||||
|
||||
self.assertEqual(get_llama_stack_config_dir(), temp_path / "config" / "llama-stack")
|
||||
self.assertEqual(get_llama_stack_data_dir(), temp_path / "data" / "llama-stack")
|
||||
self.assertEqual(get_llama_stack_state_dir(), temp_path / "state" / "llama-stack")
|
||||
self.assertEqual(get_llama_stack_cache_dir(), temp_path / "cache" / "llama-stack")
|
||||
|
||||
def test_legacy_environment_variable_precedence(self):
|
||||
"""Test that LLAMA_STACK_CONFIG_DIR takes highest precedence."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
legacy_path = Path(temp_dir) / "legacy"
|
||||
xdg_path = Path(temp_dir) / "xdg"
|
||||
|
||||
# Set both legacy and XDG variables
|
||||
os.environ["LLAMA_STACK_CONFIG_DIR"] = str(legacy_path)
|
||||
os.environ["XDG_CONFIG_HOME"] = str(xdg_path / "config")
|
||||
os.environ["XDG_DATA_HOME"] = str(xdg_path / "data")
|
||||
os.environ["XDG_STATE_HOME"] = str(xdg_path / "state")
|
||||
|
||||
# Legacy should take precedence for all directory types
|
||||
self.assertEqual(get_llama_stack_config_dir(), legacy_path)
|
||||
self.assertEqual(get_llama_stack_data_dir(), legacy_path)
|
||||
self.assertEqual(get_llama_stack_state_dir(), legacy_path)
|
||||
self.assertEqual(get_llama_stack_cache_dir(), legacy_path)
|
||||
|
||||
def test_legacy_directory_exists_and_has_content(self):
|
||||
"""Test that existing ~/.llama directory with content is used."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
home = Path(temp_dir)
|
||||
legacy_llama = home / ".llama"
|
||||
legacy_llama.mkdir()
|
||||
|
||||
# Create some content to simulate existing data
|
||||
(legacy_llama / "test_file").touch()
|
||||
(legacy_llama / "distributions").mkdir()
|
||||
|
||||
# Clear environment variables
|
||||
self.clear_env_vars()
|
||||
|
||||
with patch("llama_stack.distribution.utils.xdg_utils.Path.home") as mock_home:
|
||||
mock_home.return_value = home
|
||||
|
||||
self.assertEqual(get_llama_stack_config_dir(), legacy_llama)
|
||||
self.assertEqual(get_llama_stack_data_dir(), legacy_llama)
|
||||
self.assertEqual(get_llama_stack_state_dir(), legacy_llama)
|
||||
|
||||
def test_legacy_directory_exists_but_empty(self):
|
||||
"""Test that empty ~/.llama directory is ignored in favor of XDG."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
home = Path(temp_dir)
|
||||
legacy_llama = home / ".llama"
|
||||
legacy_llama.mkdir()
|
||||
# Don't add any content - directory is empty
|
||||
|
||||
self.clear_env_vars()
|
||||
|
||||
with patch("llama_stack.distribution.utils.xdg_utils.Path.home") as mock_home:
|
||||
mock_home.return_value = home
|
||||
|
||||
# Should use XDG paths since legacy directory is empty
|
||||
self.assertEqual(get_llama_stack_config_dir(), home / ".config" / "llama-stack")
|
||||
self.assertEqual(get_llama_stack_data_dir(), home / ".local" / "share" / "llama-stack")
|
||||
self.assertEqual(get_llama_stack_state_dir(), home / ".local" / "state" / "llama-stack")
|
||||
|
||||
def test_xdg_compliant_path_function(self):
|
||||
"""Test the get_xdg_compliant_path utility function."""
|
||||
self.clear_env_vars()
|
||||
home = Path.home()
|
||||
|
||||
# Mock that ~/.llama doesn't exist
|
||||
with patch("llama_stack.distribution.utils.xdg_utils.Path.home") as mock_home:
|
||||
mock_home.return_value = home
|
||||
with patch("llama_stack.distribution.utils.xdg_utils.Path.exists") as mock_exists:
|
||||
mock_exists.return_value = False
|
||||
|
||||
self.assertEqual(get_xdg_compliant_path("config"), home / ".config" / "llama-stack")
|
||||
self.assertEqual(
|
||||
get_xdg_compliant_path("data", "models"), home / ".local" / "share" / "llama-stack" / "models"
|
||||
)
|
||||
self.assertEqual(
|
||||
get_xdg_compliant_path("state", "runtime"), home / ".local" / "state" / "llama-stack" / "runtime"
|
||||
)
|
||||
self.assertEqual(get_xdg_compliant_path("cache", "temp"), home / ".cache" / "llama-stack" / "temp")
|
||||
|
||||
def test_xdg_compliant_path_invalid_type(self):
|
||||
"""Test that invalid path types raise ValueError."""
|
||||
with self.assertRaises(ValueError) as context:
|
||||
get_xdg_compliant_path("invalid_type")
|
||||
|
||||
self.assertIn("Unknown path type", str(context.exception))
|
||||
self.assertIn("invalid_type", str(context.exception))
|
||||
|
||||
def test_xdg_compliant_path_with_subdirectory(self):
|
||||
"""Test get_xdg_compliant_path with various subdirectories."""
|
||||
self.clear_env_vars()
|
||||
home = Path.home()
|
||||
|
||||
with patch("llama_stack.distribution.utils.xdg_utils.Path.home") as mock_home:
|
||||
mock_home.return_value = home
|
||||
with patch("llama_stack.distribution.utils.xdg_utils.Path.exists") as mock_exists:
|
||||
mock_exists.return_value = False
|
||||
|
||||
# Test nested subdirectories
|
||||
self.assertEqual(
|
||||
get_xdg_compliant_path("data", "models/checkpoints"),
|
||||
home / ".local" / "share" / "llama-stack" / "models/checkpoints",
|
||||
)
|
||||
|
||||
# Test with Path object
|
||||
self.assertEqual(
|
||||
get_xdg_compliant_path("config", str(Path("distributions") / "ollama")),
|
||||
home / ".config" / "llama-stack" / "distributions" / "ollama",
|
||||
)
|
||||
|
||||
def test_ensure_directory_exists(self):
|
||||
"""Test the ensure_directory_exists utility function."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
test_path = Path(temp_dir) / "nested" / "directory" / "structure"
|
||||
|
||||
# Directory shouldn't exist initially
|
||||
self.assertFalse(test_path.exists())
|
||||
|
||||
# Create it
|
||||
ensure_directory_exists(test_path)
|
||||
|
||||
# Should exist now
|
||||
self.assertTrue(test_path.exists())
|
||||
self.assertTrue(test_path.is_dir())
|
||||
|
||||
def test_ensure_directory_exists_already_exists(self):
|
||||
"""Test ensure_directory_exists when directory already exists."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
test_path = Path(temp_dir) / "existing"
|
||||
test_path.mkdir()
|
||||
|
||||
# Should not raise an error
|
||||
ensure_directory_exists(test_path)
|
||||
self.assertTrue(test_path.exists())
|
||||
|
||||
def test_config_dirs_import_and_types(self):
|
||||
"""Test that the config_dirs module imports correctly and has proper types."""
|
||||
from llama_stack.distribution.utils.config_dirs import (
|
||||
DEFAULT_CHECKPOINT_DIR,
|
||||
DISTRIBS_BASE_DIR,
|
||||
EXTERNAL_PROVIDERS_DIR,
|
||||
LLAMA_STACK_CONFIG_DIR,
|
||||
RUNTIME_BASE_DIR,
|
||||
)
|
||||
|
||||
# All should be Path objects
|
||||
self.assertIsInstance(LLAMA_STACK_CONFIG_DIR, Path)
|
||||
self.assertIsInstance(DEFAULT_CHECKPOINT_DIR, Path)
|
||||
self.assertIsInstance(RUNTIME_BASE_DIR, Path)
|
||||
self.assertIsInstance(EXTERNAL_PROVIDERS_DIR, Path)
|
||||
self.assertIsInstance(DISTRIBS_BASE_DIR, Path)
|
||||
|
||||
# All should be absolute paths
|
||||
self.assertTrue(LLAMA_STACK_CONFIG_DIR.is_absolute())
|
||||
self.assertTrue(DEFAULT_CHECKPOINT_DIR.is_absolute())
|
||||
self.assertTrue(RUNTIME_BASE_DIR.is_absolute())
|
||||
self.assertTrue(EXTERNAL_PROVIDERS_DIR.is_absolute())
|
||||
self.assertTrue(DISTRIBS_BASE_DIR.is_absolute())
|
||||
|
||||
def test_config_dirs_proper_structure(self):
|
||||
"""Test that config_dirs uses proper XDG structure."""
|
||||
from llama_stack.distribution.utils.config_dirs import (
|
||||
DISTRIBS_BASE_DIR,
|
||||
EXTERNAL_PROVIDERS_DIR,
|
||||
LLAMA_STACK_CONFIG_DIR,
|
||||
)
|
||||
|
||||
# Check that paths contain expected components
|
||||
config_str = str(LLAMA_STACK_CONFIG_DIR)
|
||||
self.assertTrue(
|
||||
"llama-stack" in config_str or ".llama" in config_str,
|
||||
f"Config dir should contain 'llama-stack' or '.llama': {config_str}",
|
||||
)
|
||||
|
||||
# Test relationships between directories
|
||||
self.assertEqual(DISTRIBS_BASE_DIR, LLAMA_STACK_CONFIG_DIR / "distributions")
|
||||
self.assertEqual(EXTERNAL_PROVIDERS_DIR, LLAMA_STACK_CONFIG_DIR / "providers.d")
|
||||
|
||||
def test_environment_variable_combinations(self):
|
||||
"""Test various combinations of environment variables."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
temp_path = Path(temp_dir)
|
||||
|
||||
# Test partial XDG variables
|
||||
os.environ["XDG_CONFIG_HOME"] = str(temp_path / "config")
|
||||
# Leave others as default
|
||||
self.clear_env_vars(["XDG_DATA_HOME", "XDG_STATE_HOME", "XDG_CACHE_HOME"])
|
||||
|
||||
home = Path.home()
|
||||
|
||||
with patch("llama_stack.distribution.utils.xdg_utils.Path.exists") as mock_exists:
|
||||
mock_exists.return_value = False
|
||||
|
||||
self.assertEqual(get_llama_stack_config_dir(), temp_path / "config" / "llama-stack")
|
||||
self.assertEqual(get_llama_stack_data_dir(), home / ".local" / "share" / "llama-stack")
|
||||
self.assertEqual(get_llama_stack_state_dir(), home / ".local" / "state" / "llama-stack")
|
||||
|
||||
def test_migrate_legacy_directory_no_legacy(self):
|
||||
"""Test migration when no legacy directory exists."""
|
||||
with patch("llama_stack.distribution.utils.xdg_utils.Path.home") as mock_home:
|
||||
mock_home.return_value = Path("/fake/home")
|
||||
with patch("llama_stack.distribution.utils.xdg_utils.Path.exists") as mock_exists:
|
||||
mock_exists.return_value = False
|
||||
|
||||
# Should return True (success) when no migration needed
|
||||
result = migrate_legacy_directory()
|
||||
self.assertTrue(result)
|
||||
|
||||
def test_migrate_legacy_directory_exists(self):
|
||||
"""Test migration message when legacy directory exists."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
home = Path(temp_dir)
|
||||
legacy_llama = home / ".llama"
|
||||
legacy_llama.mkdir()
|
||||
(legacy_llama / "test_file").touch()
|
||||
|
||||
with patch("llama_stack.distribution.utils.xdg_utils.Path.home") as mock_home:
|
||||
mock_home.return_value = home
|
||||
with patch("builtins.print") as mock_print:
|
||||
result = migrate_legacy_directory()
|
||||
self.assertTrue(result)
|
||||
|
||||
# Check that migration information was printed
|
||||
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))
|
||||
self.assertTrue(any("Consider migrating" in call for call in print_calls))
|
||||
|
||||
def test_path_consistency_across_functions(self):
|
||||
"""Test that all path functions return consistent results."""
|
||||
self.clear_env_vars()
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
home = Path(temp_dir)
|
||||
|
||||
with patch("llama_stack.distribution.utils.xdg_utils.Path.home") as mock_home:
|
||||
mock_home.return_value = home
|
||||
with patch("llama_stack.distribution.utils.xdg_utils.Path.exists") as mock_exists:
|
||||
mock_exists.return_value = False
|
||||
|
||||
# All config-related functions should return the same base
|
||||
config_dir = get_llama_stack_config_dir()
|
||||
config_path = get_xdg_compliant_path("config")
|
||||
self.assertEqual(config_dir, config_path)
|
||||
|
||||
# All data-related functions should return the same base
|
||||
data_dir = get_llama_stack_data_dir()
|
||||
data_path = get_xdg_compliant_path("data")
|
||||
self.assertEqual(data_dir, data_path)
|
||||
|
||||
def test_unicode_and_special_characters(self):
|
||||
"""Test XDG paths with unicode and special characters."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
# Test with unicode characters
|
||||
unicode_path = Path(temp_dir) / "配置" / "llama-stack"
|
||||
os.environ["XDG_CONFIG_HOME"] = str(unicode_path.parent)
|
||||
|
||||
result = get_xdg_config_home()
|
||||
self.assertEqual(result, unicode_path.parent)
|
||||
|
||||
# Test spaces in paths
|
||||
space_path = Path(temp_dir) / "my config"
|
||||
os.environ["XDG_CONFIG_HOME"] = str(space_path)
|
||||
|
||||
result = get_xdg_config_home()
|
||||
self.assertEqual(result, space_path)
|
||||
|
||||
def test_concurrent_access_safety(self):
|
||||
"""Test that XDG functions are safe for concurrent access."""
|
||||
import threading
|
||||
import time
|
||||
|
||||
results = []
|
||||
errors = []
|
||||
|
||||
def worker():
|
||||
try:
|
||||
# Simulate concurrent access
|
||||
config_dir = get_llama_stack_config_dir()
|
||||
time.sleep(0.01) # Small delay to increase chance of race conditions
|
||||
data_dir = get_llama_stack_data_dir()
|
||||
results.append((config_dir, data_dir))
|
||||
except Exception as e:
|
||||
errors.append(e)
|
||||
|
||||
# Start multiple threads
|
||||
threads = []
|
||||
for _ in range(10):
|
||||
t = threading.Thread(target=worker)
|
||||
threads.append(t)
|
||||
t.start()
|
||||
|
||||
# Wait for all threads
|
||||
for t in threads:
|
||||
t.join()
|
||||
|
||||
# Check results
|
||||
self.assertEqual(len(errors), 0, f"Concurrent access errors: {errors}")
|
||||
self.assertEqual(len(results), 10)
|
||||
|
||||
# All results should be identical
|
||||
first_result = results[0]
|
||||
for result in results[1:]:
|
||||
self.assertEqual(result, first_result)
|
||||
|
||||
def test_symlink_handling(self):
|
||||
"""Test XDG path handling with symbolic links."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
temp_path = Path(temp_dir)
|
||||
|
||||
# Create actual directory
|
||||
actual_dir = temp_path / "actual_config"
|
||||
actual_dir.mkdir()
|
||||
|
||||
# Create symlink
|
||||
symlink_dir = temp_path / "symlinked_config"
|
||||
symlink_dir.symlink_to(actual_dir)
|
||||
|
||||
os.environ["XDG_CONFIG_HOME"] = str(symlink_dir)
|
||||
|
||||
result = get_xdg_config_home()
|
||||
self.assertEqual(result, symlink_dir)
|
||||
|
||||
# Should resolve to actual path when needed
|
||||
self.assertTrue(result.exists())
|
||||
|
||||
def test_readonly_directory_handling(self):
|
||||
"""Test behavior when XDG directories are read-only."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
temp_path = Path(temp_dir)
|
||||
readonly_dir = temp_path / "readonly"
|
||||
readonly_dir.mkdir()
|
||||
|
||||
# Make directory read-only
|
||||
readonly_dir.chmod(0o444)
|
||||
|
||||
try:
|
||||
os.environ["XDG_CONFIG_HOME"] = str(readonly_dir)
|
||||
|
||||
# Should still return the path even if read-only
|
||||
result = get_xdg_config_home()
|
||||
self.assertEqual(result, readonly_dir)
|
||||
|
||||
finally:
|
||||
# Restore permissions for cleanup
|
||||
readonly_dir.chmod(0o755)
|
||||
|
||||
def test_nonexistent_parent_directory(self):
|
||||
"""Test XDG paths with non-existent parent directories."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
# Use a path with non-existent parents
|
||||
nonexistent_path = Path(temp_dir) / "does" / "not" / "exist" / "config"
|
||||
|
||||
os.environ["XDG_CONFIG_HOME"] = str(nonexistent_path)
|
||||
|
||||
# Should return the path even if it doesn't exist
|
||||
result = get_xdg_config_home()
|
||||
self.assertEqual(result, nonexistent_path)
|
||||
|
||||
def test_env_var_expansion(self):
|
||||
"""Test environment variable expansion in XDG paths."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
os.environ["TEST_BASE"] = temp_dir
|
||||
os.environ["XDG_CONFIG_HOME"] = "$TEST_BASE/config"
|
||||
|
||||
# Path expansion should work
|
||||
result = get_xdg_config_home()
|
||||
expected = Path(temp_dir) / "config"
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
|
||||
class TestXDGEdgeCases(unittest.TestCase):
|
||||
"""Test edge cases and error conditions for XDG compliance."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test environment."""
|
||||
self.original_env = {}
|
||||
for key in ["XDG_CONFIG_HOME", "XDG_DATA_HOME", "XDG_CACHE_HOME", "XDG_STATE_HOME", "LLAMA_STACK_CONFIG_DIR"]:
|
||||
self.original_env[key] = os.environ.get(key)
|
||||
|
||||
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 test_empty_environment_variables(self):
|
||||
"""Test behavior with empty environment variables."""
|
||||
# Set empty values
|
||||
os.environ["XDG_CONFIG_HOME"] = ""
|
||||
os.environ["XDG_DATA_HOME"] = ""
|
||||
|
||||
# Should fall back to defaults
|
||||
home = Path.home()
|
||||
self.assertEqual(get_xdg_config_home(), home / ".config")
|
||||
self.assertEqual(get_xdg_data_home(), home / ".local" / "share")
|
||||
|
||||
def test_whitespace_only_environment_variables(self):
|
||||
"""Test behavior with whitespace-only environment variables."""
|
||||
os.environ["XDG_CONFIG_HOME"] = " "
|
||||
os.environ["XDG_DATA_HOME"] = "\t\n"
|
||||
|
||||
# Should handle whitespace gracefully
|
||||
result_config = get_xdg_config_home()
|
||||
result_data = get_xdg_data_home()
|
||||
|
||||
# Results should be valid Path objects
|
||||
self.assertIsInstance(result_config, Path)
|
||||
self.assertIsInstance(result_data, Path)
|
||||
|
||||
def test_very_long_paths(self):
|
||||
"""Test behavior with very long directory paths."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
# Create a very long path
|
||||
long_path_parts = ["very_long_directory_name_" + str(i) for i in range(20)]
|
||||
long_path = Path(temp_dir)
|
||||
for part in long_path_parts:
|
||||
long_path = long_path / part
|
||||
|
||||
os.environ["XDG_CONFIG_HOME"] = str(long_path)
|
||||
|
||||
result = get_xdg_config_home()
|
||||
self.assertEqual(result, long_path)
|
||||
|
||||
def test_circular_symlinks(self):
|
||||
"""Test handling of circular symbolic links."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
temp_path = Path(temp_dir)
|
||||
|
||||
# Create circular symlinks
|
||||
link1 = temp_path / "link1"
|
||||
link2 = temp_path / "link2"
|
||||
|
||||
try:
|
||||
link1.symlink_to(link2)
|
||||
link2.symlink_to(link1)
|
||||
|
||||
os.environ["XDG_CONFIG_HOME"] = str(link1)
|
||||
|
||||
# Should handle circular symlinks gracefully
|
||||
result = get_xdg_config_home()
|
||||
self.assertEqual(result, link1)
|
||||
|
||||
except (OSError, NotImplementedError):
|
||||
# Some systems don't support circular symlinks
|
||||
self.skipTest("System doesn't support circular symlinks")
|
||||
|
||||
def test_permission_denied_scenarios(self):
|
||||
"""Test scenarios where permission is denied."""
|
||||
# This test is platform-specific and may not work on all systems
|
||||
try:
|
||||
# Try to use a system directory that typically requires root
|
||||
os.environ["XDG_CONFIG_HOME"] = "/root/.config"
|
||||
|
||||
# Should still return the path even if we can't access it
|
||||
result = get_xdg_config_home()
|
||||
self.assertEqual(result, Path("/root/.config"))
|
||||
|
||||
except Exception:
|
||||
# If this fails, it's not critical for the XDG implementation
|
||||
pass
|
||||
|
||||
def test_network_paths(self):
|
||||
"""Test XDG paths with network/UNC paths (Windows-style)."""
|
||||
# Test UNC path (though this may not work on non-Windows systems)
|
||||
network_path = "//server/share/config"
|
||||
os.environ["XDG_CONFIG_HOME"] = network_path
|
||||
|
||||
result = get_xdg_config_home()
|
||||
# Should handle network paths gracefully
|
||||
self.assertIsInstance(result, Path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
522
tests/unit/test_xdg_edge_cases.py
Normal file
522
tests/unit/test_xdg_edge_cases.py
Normal file
|
@ -0,0 +1,522 @@
|
|||
# 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 os
|
||||
import platform
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from llama_stack.distribution.utils.xdg_utils import (
|
||||
ensure_directory_exists,
|
||||
get_llama_stack_config_dir,
|
||||
get_xdg_config_home,
|
||||
migrate_legacy_directory,
|
||||
)
|
||||
|
||||
|
||||
class TestXDGEdgeCases(unittest.TestCase):
|
||||
"""Test edge cases and error conditions for XDG compliance."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test environment."""
|
||||
self.original_env = {}
|
||||
for key in ["XDG_CONFIG_HOME", "XDG_DATA_HOME", "XDG_STATE_HOME", "XDG_CACHE_HOME", "LLAMA_STACK_CONFIG_DIR"]:
|
||||
self.original_env[key] = os.environ.get(key)
|
||||
|
||||
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 clear_env_vars(self):
|
||||
"""Clear all XDG environment variables."""
|
||||
for key in self.original_env:
|
||||
os.environ.pop(key, None)
|
||||
|
||||
def test_very_long_paths(self):
|
||||
"""Test XDG functions with very long directory paths."""
|
||||
self.clear_env_vars()
|
||||
|
||||
# Create a very long path (close to filesystem limits)
|
||||
long_components = ["very_long_directory_name_" + str(i) for i in range(50)]
|
||||
long_path = "/tmp/" + "/".join(long_components)
|
||||
|
||||
# Test with very long XDG paths
|
||||
os.environ["XDG_CONFIG_HOME"] = long_path
|
||||
|
||||
result = get_xdg_config_home()
|
||||
self.assertEqual(result, Path(long_path))
|
||||
|
||||
# Should handle long paths in llama-stack functions
|
||||
with patch("llama_stack.distribution.utils.xdg_utils.Path.exists") as mock_exists:
|
||||
mock_exists.return_value = False
|
||||
|
||||
config_dir = get_llama_stack_config_dir()
|
||||
self.assertEqual(config_dir, Path(long_path) / "llama-stack")
|
||||
|
||||
def test_paths_with_special_characters(self):
|
||||
"""Test XDG functions with special characters in paths."""
|
||||
self.clear_env_vars()
|
||||
|
||||
# Test various special characters
|
||||
special_chars = [
|
||||
"path with spaces",
|
||||
"path-with-hyphens",
|
||||
"path_with_underscores",
|
||||
"path.with.dots",
|
||||
"path@with@symbols",
|
||||
"path+with+plus",
|
||||
"path&with&ersand",
|
||||
"path(with)parentheses",
|
||||
]
|
||||
|
||||
for special_path in special_chars:
|
||||
with self.subTest(path=special_path):
|
||||
test_path = f"/tmp/{special_path}"
|
||||
os.environ["XDG_CONFIG_HOME"] = test_path
|
||||
|
||||
result = get_xdg_config_home()
|
||||
self.assertEqual(result, Path(test_path))
|
||||
|
||||
def test_unicode_paths(self):
|
||||
"""Test XDG functions with unicode characters in paths."""
|
||||
self.clear_env_vars()
|
||||
|
||||
unicode_paths = [
|
||||
"/配置/llama-stack", # Chinese
|
||||
"/конфигурация/llama-stack", # Russian
|
||||
"/構成/llama-stack", # Japanese
|
||||
"/구성/llama-stack", # Korean
|
||||
"/تكوين/llama-stack", # Arabic
|
||||
"/configuración/llama-stack", # Spanish with accents
|
||||
"/配置📁/llama-stack", # With emoji
|
||||
]
|
||||
|
||||
for unicode_path in unicode_paths:
|
||||
with self.subTest(path=unicode_path):
|
||||
os.environ["XDG_CONFIG_HOME"] = unicode_path
|
||||
|
||||
result = get_xdg_config_home()
|
||||
self.assertEqual(result, Path(unicode_path))
|
||||
|
||||
def test_network_paths(self):
|
||||
"""Test XDG functions with network/UNC paths."""
|
||||
self.clear_env_vars()
|
||||
|
||||
if platform.system() == "Windows":
|
||||
# Test Windows UNC paths
|
||||
unc_paths = [
|
||||
"\\\\server\\share\\config",
|
||||
"\\\\server.domain.com\\share\\config",
|
||||
"\\\\192.168.1.100\\config",
|
||||
]
|
||||
|
||||
for unc_path in unc_paths:
|
||||
with self.subTest(path=unc_path):
|
||||
os.environ["XDG_CONFIG_HOME"] = unc_path
|
||||
|
||||
result = get_xdg_config_home()
|
||||
self.assertEqual(result, Path(unc_path))
|
||||
else:
|
||||
# Test network mount paths on Unix-like systems
|
||||
network_paths = [
|
||||
"/mnt/nfs/config",
|
||||
"/net/server/config",
|
||||
"/media/network/config",
|
||||
]
|
||||
|
||||
for network_path in network_paths:
|
||||
with self.subTest(path=network_path):
|
||||
os.environ["XDG_CONFIG_HOME"] = network_path
|
||||
|
||||
result = get_xdg_config_home()
|
||||
self.assertEqual(result, Path(network_path))
|
||||
|
||||
def test_nonexistent_paths(self):
|
||||
"""Test XDG functions with non-existent paths."""
|
||||
self.clear_env_vars()
|
||||
|
||||
nonexistent_path = "/this/path/does/not/exist/config"
|
||||
os.environ["XDG_CONFIG_HOME"] = nonexistent_path
|
||||
|
||||
# Should return the path even if it doesn't exist
|
||||
result = get_xdg_config_home()
|
||||
self.assertEqual(result, Path(nonexistent_path))
|
||||
|
||||
# Should work with llama-stack functions too
|
||||
with patch("llama_stack.distribution.utils.xdg_utils.Path.exists") as mock_exists:
|
||||
mock_exists.return_value = False
|
||||
|
||||
config_dir = get_llama_stack_config_dir()
|
||||
self.assertEqual(config_dir, Path(nonexistent_path) / "llama-stack")
|
||||
|
||||
def test_circular_symlinks(self):
|
||||
"""Test XDG functions with circular symbolic links."""
|
||||
self.clear_env_vars()
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
temp_path = Path(temp_dir)
|
||||
|
||||
# Create circular symlinks
|
||||
link1 = temp_path / "link1"
|
||||
link2 = temp_path / "link2"
|
||||
|
||||
try:
|
||||
link1.symlink_to(link2)
|
||||
link2.symlink_to(link1)
|
||||
|
||||
os.environ["XDG_CONFIG_HOME"] = str(link1)
|
||||
|
||||
# Should handle circular symlinks gracefully
|
||||
result = get_xdg_config_home()
|
||||
self.assertEqual(result, link1)
|
||||
|
||||
except (OSError, NotImplementedError):
|
||||
# Some systems don't support circular symlinks
|
||||
self.skipTest("System doesn't support circular symlinks")
|
||||
|
||||
def test_broken_symlinks(self):
|
||||
"""Test XDG functions with broken symbolic links."""
|
||||
self.clear_env_vars()
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
temp_path = Path(temp_dir)
|
||||
|
||||
# Create broken symlink
|
||||
target = temp_path / "nonexistent_target"
|
||||
link = temp_path / "broken_link"
|
||||
|
||||
try:
|
||||
link.symlink_to(target)
|
||||
|
||||
os.environ["XDG_CONFIG_HOME"] = str(link)
|
||||
|
||||
# Should handle broken symlinks gracefully
|
||||
result = get_xdg_config_home()
|
||||
self.assertEqual(result, link)
|
||||
|
||||
except (OSError, NotImplementedError):
|
||||
# Some systems might not support this
|
||||
self.skipTest("System doesn't support broken symlinks")
|
||||
|
||||
def test_readonly_directories(self):
|
||||
"""Test XDG functions with read-only directories."""
|
||||
self.clear_env_vars()
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
temp_path = Path(temp_dir)
|
||||
readonly_dir = temp_path / "readonly"
|
||||
readonly_dir.mkdir()
|
||||
|
||||
# Make directory read-only
|
||||
readonly_dir.chmod(0o444)
|
||||
|
||||
try:
|
||||
os.environ["XDG_CONFIG_HOME"] = str(readonly_dir)
|
||||
|
||||
# Should still return the path
|
||||
result = get_xdg_config_home()
|
||||
self.assertEqual(result, readonly_dir)
|
||||
|
||||
finally:
|
||||
# Restore permissions for cleanup
|
||||
readonly_dir.chmod(0o755)
|
||||
|
||||
def test_permission_denied_access(self):
|
||||
"""Test XDG functions when permission is denied."""
|
||||
self.clear_env_vars()
|
||||
|
||||
# This test is platform-specific
|
||||
if platform.system() != "Windows":
|
||||
# Try to use a system directory that typically requires root
|
||||
restricted_paths = [
|
||||
"/root/.config",
|
||||
"/etc/config",
|
||||
"/var/root/config",
|
||||
]
|
||||
|
||||
for restricted_path in restricted_paths:
|
||||
with self.subTest(path=restricted_path):
|
||||
os.environ["XDG_CONFIG_HOME"] = restricted_path
|
||||
|
||||
# Should still return the path even if we can't access it
|
||||
result = get_xdg_config_home()
|
||||
self.assertEqual(result, Path(restricted_path))
|
||||
|
||||
def test_environment_variable_injection(self):
|
||||
"""Test XDG functions with environment variable injection attempts."""
|
||||
self.clear_env_vars()
|
||||
|
||||
# Test potential injection attempts
|
||||
injection_attempts = [
|
||||
"/tmp/config; rm -rf /",
|
||||
"/tmp/config && echo 'injected'",
|
||||
"/tmp/config | cat /etc/passwd",
|
||||
"/tmp/config`whoami`",
|
||||
"/tmp/config$(whoami)",
|
||||
"/tmp/config\necho 'newline'",
|
||||
]
|
||||
|
||||
for injection_attempt in injection_attempts:
|
||||
with self.subTest(attempt=injection_attempt):
|
||||
os.environ["XDG_CONFIG_HOME"] = injection_attempt
|
||||
|
||||
# Should treat as literal path, not execute
|
||||
result = get_xdg_config_home()
|
||||
self.assertEqual(result, Path(injection_attempt))
|
||||
|
||||
def test_extremely_nested_paths(self):
|
||||
"""Test XDG functions with extremely nested directory structures."""
|
||||
self.clear_env_vars()
|
||||
|
||||
# Create deeply nested path
|
||||
nested_parts = ["level" + str(i) for i in range(100)]
|
||||
nested_path = "/tmp/" + "/".join(nested_parts)
|
||||
|
||||
os.environ["XDG_CONFIG_HOME"] = nested_path
|
||||
|
||||
result = get_xdg_config_home()
|
||||
self.assertEqual(result, Path(nested_path))
|
||||
|
||||
def test_empty_and_whitespace_paths(self):
|
||||
"""Test XDG functions with empty and whitespace-only paths."""
|
||||
self.clear_env_vars()
|
||||
|
||||
empty_values = [
|
||||
"",
|
||||
" ",
|
||||
"\t",
|
||||
"\n",
|
||||
"\r\n",
|
||||
" \t \n ",
|
||||
]
|
||||
|
||||
for empty_value in empty_values:
|
||||
with self.subTest(value=repr(empty_value)):
|
||||
os.environ["XDG_CONFIG_HOME"] = empty_value
|
||||
|
||||
# Should fall back to default
|
||||
result = get_xdg_config_home()
|
||||
self.assertEqual(result, Path.home() / ".config")
|
||||
|
||||
def test_path_with_null_bytes(self):
|
||||
"""Test XDG functions with null bytes in paths."""
|
||||
self.clear_env_vars()
|
||||
|
||||
# Test path with null byte
|
||||
null_path = "/tmp/config\x00/test"
|
||||
os.environ["XDG_CONFIG_HOME"] = null_path
|
||||
|
||||
# Should handle null bytes (Path will likely raise an error, which is expected)
|
||||
try:
|
||||
result = get_xdg_config_home()
|
||||
# If it doesn't raise an error, check the result
|
||||
self.assertIsInstance(result, Path)
|
||||
except (ValueError, OSError):
|
||||
# This is expected behavior for null bytes
|
||||
pass
|
||||
|
||||
def test_concurrent_access_safety(self):
|
||||
"""Test that XDG functions are thread-safe."""
|
||||
import threading
|
||||
import time
|
||||
|
||||
self.clear_env_vars()
|
||||
|
||||
results = []
|
||||
errors = []
|
||||
|
||||
def worker(thread_id):
|
||||
try:
|
||||
# Each thread sets a different XDG path
|
||||
os.environ["XDG_CONFIG_HOME"] = f"/tmp/thread_{thread_id}"
|
||||
|
||||
# Small delay to increase chance of race conditions
|
||||
time.sleep(0.01)
|
||||
|
||||
config_dir = get_llama_stack_config_dir()
|
||||
results.append((thread_id, config_dir))
|
||||
|
||||
except Exception as e:
|
||||
errors.append((thread_id, e))
|
||||
|
||||
# Start multiple threads
|
||||
threads = []
|
||||
for i in range(20):
|
||||
t = threading.Thread(target=worker, args=(i,))
|
||||
threads.append(t)
|
||||
t.start()
|
||||
|
||||
# Wait for all threads
|
||||
for t in threads:
|
||||
t.join()
|
||||
|
||||
# Check for errors
|
||||
if errors:
|
||||
self.fail(f"Thread errors: {errors}")
|
||||
|
||||
# Check that we got results from all threads
|
||||
self.assertEqual(len(results), 20)
|
||||
|
||||
def test_filesystem_limits(self):
|
||||
"""Test XDG functions approaching filesystem limits."""
|
||||
self.clear_env_vars()
|
||||
|
||||
# Test with very long filename (close to 255 char limit)
|
||||
long_filename = "a" * 240
|
||||
long_path = f"/tmp/{long_filename}"
|
||||
|
||||
os.environ["XDG_CONFIG_HOME"] = long_path
|
||||
|
||||
result = get_xdg_config_home()
|
||||
self.assertEqual(result, Path(long_path))
|
||||
|
||||
def test_case_sensitivity(self):
|
||||
"""Test XDG functions with case sensitivity edge cases."""
|
||||
self.clear_env_vars()
|
||||
|
||||
# Test case variations
|
||||
case_variations = [
|
||||
"/tmp/Config",
|
||||
"/tmp/CONFIG",
|
||||
"/tmp/config",
|
||||
"/tmp/Config/MixedCase",
|
||||
]
|
||||
|
||||
for case_path in case_variations:
|
||||
with self.subTest(path=case_path):
|
||||
os.environ["XDG_CONFIG_HOME"] = case_path
|
||||
|
||||
result = get_xdg_config_home()
|
||||
self.assertEqual(result, Path(case_path))
|
||||
|
||||
def test_ensure_directory_exists_edge_cases(self):
|
||||
"""Test ensure_directory_exists with edge cases."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
temp_path = Path(temp_dir)
|
||||
|
||||
# Test with file that exists but is not a directory
|
||||
file_path = temp_path / "file_not_dir"
|
||||
file_path.touch()
|
||||
|
||||
with self.assertRaises(FileExistsError):
|
||||
ensure_directory_exists(file_path)
|
||||
|
||||
# Test with permission denied
|
||||
if platform.system() != "Windows":
|
||||
readonly_parent = temp_path / "readonly_parent"
|
||||
readonly_parent.mkdir()
|
||||
readonly_parent.chmod(0o444)
|
||||
|
||||
try:
|
||||
nested_path = readonly_parent / "nested"
|
||||
|
||||
with self.assertRaises(PermissionError):
|
||||
ensure_directory_exists(nested_path)
|
||||
|
||||
finally:
|
||||
# Restore permissions for cleanup
|
||||
readonly_parent.chmod(0o755)
|
||||
|
||||
def test_migrate_legacy_directory_edge_cases(self):
|
||||
"""Test migrate_legacy_directory with edge cases."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
home_dir = Path(temp_dir)
|
||||
|
||||
with patch("llama_stack.distribution.utils.xdg_utils.Path.home") as mock_home:
|
||||
mock_home.return_value = home_dir
|
||||
|
||||
# Test with legacy directory but no write permissions
|
||||
legacy_dir = home_dir / ".llama"
|
||||
legacy_dir.mkdir()
|
||||
(legacy_dir / "test_file").touch()
|
||||
|
||||
# Make home directory read-only
|
||||
home_dir.chmod(0o444)
|
||||
|
||||
try:
|
||||
# Should handle permission errors gracefully
|
||||
with patch("builtins.print") as mock_print:
|
||||
migrate_legacy_directory()
|
||||
|
||||
# Should print some information
|
||||
self.assertTrue(mock_print.called)
|
||||
|
||||
finally:
|
||||
# Restore permissions for cleanup
|
||||
home_dir.chmod(0o755)
|
||||
legacy_dir.chmod(0o755)
|
||||
|
||||
def test_path_traversal_attempts(self):
|
||||
"""Test XDG functions with path traversal attempts."""
|
||||
self.clear_env_vars()
|
||||
|
||||
traversal_attempts = [
|
||||
"/tmp/config/../../../etc/passwd",
|
||||
"/tmp/config/../../root/.ssh",
|
||||
"/tmp/config/../../../../../etc/shadow",
|
||||
"/tmp/config/./../../root",
|
||||
]
|
||||
|
||||
for traversal_attempt in traversal_attempts:
|
||||
with self.subTest(attempt=traversal_attempt):
|
||||
os.environ["XDG_CONFIG_HOME"] = traversal_attempt
|
||||
|
||||
# Should handle path traversal attempts by treating as literal paths
|
||||
result = get_xdg_config_home()
|
||||
self.assertEqual(result, Path(traversal_attempt))
|
||||
|
||||
def test_environment_variable_precedence_edge_cases(self):
|
||||
"""Test environment variable precedence with edge cases."""
|
||||
self.clear_env_vars()
|
||||
|
||||
# Test with both old and new env vars set
|
||||
os.environ["LLAMA_STACK_CONFIG_DIR"] = "/legacy/path"
|
||||
os.environ["XDG_CONFIG_HOME"] = "/xdg/path"
|
||||
|
||||
# Create fake legacy directory
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
fake_home = Path(temp_dir)
|
||||
fake_legacy = fake_home / ".llama"
|
||||
fake_legacy.mkdir()
|
||||
(fake_legacy / "test_file").touch()
|
||||
|
||||
with patch("llama_stack.distribution.utils.xdg_utils.Path.home") as mock_home:
|
||||
mock_home.return_value = fake_home
|
||||
|
||||
# LLAMA_STACK_CONFIG_DIR should take precedence
|
||||
config_dir = get_llama_stack_config_dir()
|
||||
self.assertEqual(config_dir, Path("/legacy/path"))
|
||||
|
||||
def test_malformed_environment_variables(self):
|
||||
"""Test XDG functions with malformed environment variables."""
|
||||
self.clear_env_vars()
|
||||
|
||||
malformed_values = [
|
||||
"not_an_absolute_path",
|
||||
"~/tilde_not_expanded",
|
||||
"$HOME/variable_not_expanded",
|
||||
"relative/path/config",
|
||||
"./relative/path",
|
||||
"../parent/path",
|
||||
]
|
||||
|
||||
for malformed_value in malformed_values:
|
||||
with self.subTest(value=malformed_value):
|
||||
os.environ["XDG_CONFIG_HOME"] = malformed_value
|
||||
|
||||
# Should handle malformed values gracefully
|
||||
result = get_xdg_config_home()
|
||||
self.assertIsInstance(result, Path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
Loading…
Add table
Add a link
Reference in a new issue