feat: use XDG directory standards

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

View file

@ -0,0 +1,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()