# SPDX-License-Identifier: Apache-2.0 """Tests for omlx.config module.""" import os from argparse import Namespace from pathlib import Path from unittest.mock import patch import pytest from omlx.config import ( parse_size, ServerConfig, ModelConfig, GenerationConfig, SchedulerConfig, CacheConfig, PagedSSDCacheConfig, MCPConfig, OMLXConfig, ) class TestParseSize: """Test cases for parse_size function.""" def test_parse_bytes(self): """Test parsing byte values.""" assert parse_size("100B") == 100 assert parse_size("0B") == 0 assert parse_size("1024B") == 1024 def test_parse_kilobytes(self): """Test parsing KB values.""" assert parse_size("1KB") == 1024 assert parse_size("100KB") == 100 * 1024 assert parse_size("1.5KB") == int(1.5 * 1024) def test_parse_megabytes(self): """Test parsing MB values.""" assert parse_size("1MB") == 1024**2 assert parse_size("512MB") == 512 * 1024**2 assert parse_size("2.5MB") == int(2.5 * 1024**2) def test_parse_gigabytes(self): """Test parsing GB values.""" assert parse_size("1GB") == 1024**3 assert parse_size("16GB") == 16 * 1024**3 assert parse_size("32.5GB") == int(32.5 * 1024**3) def test_parse_terabytes(self): """Test parsing TB values.""" assert parse_size("1TB") == 1024**4 assert parse_size("2TB") == 2 * 1024**4 def test_parse_case_insensitive(self): """Test that parsing is case-insensitive.""" assert parse_size("1gb") == 1024**3 assert parse_size("1Gb") == 1024**3 assert parse_size("1gB") == 1024**3 assert parse_size("1GB") == 1024**3 def test_parse_with_whitespace(self): """Test parsing with leading/trailing whitespace.""" assert parse_size(" 1GB ") == 1024**3 assert parse_size("\t16GB\n") == 16 * 1024**3 def test_parse_plain_number(self): """Test parsing plain number as bytes.""" assert parse_size("1024") == 1024 assert parse_size("0") == 0 def test_parse_invalid_raises_error(self): """Test that invalid input raises ValueError.""" with pytest.raises(ValueError): parse_size("invalid") with pytest.raises(ValueError): parse_size("abc123") with pytest.raises(ValueError): parse_size("1XB") # Invalid unit class TestServerConfig: """Test cases for ServerConfig dataclass.""" def test_default_values(self): """Test default configuration values.""" config = ServerConfig() assert config.host == "0.0.0.0" assert config.port == 8000 assert config.log_level == "info" assert config.cors_origins == ["*"] def test_custom_values(self): """Test custom configuration values.""" config = ServerConfig( host="127.0.0.1", port=9000, log_level="debug", cors_origins=["http://localhost:3000"], ) assert config.host == "127.0.0.1" assert config.port == 9000 assert config.log_level == "debug" assert config.cors_origins == ["http://localhost:3000"] class TestModelConfig: """Test cases for ModelConfig dataclass.""" def test_default_values(self): """Test default configuration values.""" config = ModelConfig() assert config.model_name == "" # Issue #926: defaults to False so HF repos can't auto-execute custom Python. assert config.trust_remote_code is False assert config.model_path is None def test_custom_values(self): """Test custom configuration values.""" config = ModelConfig( model_name="llama-3.1-8b", trust_remote_code=False, model_path="/path/to/model", ) assert config.model_name == "llama-3.1-8b" assert config.trust_remote_code is False assert config.model_path == "/path/to/model" class TestGenerationConfig: """Test cases for GenerationConfig dataclass.""" def test_default_values(self): """Test default configuration values.""" config = GenerationConfig() assert config.max_tokens == 32768 assert config.temperature == 1.0 assert config.top_p == 0.95 assert config.top_k == 0 assert config.force_sampling is False def test_custom_values(self): """Test custom configuration values.""" config = GenerationConfig( max_tokens=4096, temperature=0.7, top_p=0.9, top_k=50, force_sampling=True, ) assert config.max_tokens == 4096 assert config.temperature == 0.7 assert config.top_p == 0.9 assert config.top_k == 50 assert config.force_sampling is True class TestSchedulerConfig: """Test cases for SchedulerConfig dataclass.""" def test_default_values(self): """Test default configuration values.""" config = SchedulerConfig() assert config.max_num_seqs == 8 assert config.completion_batch_size == 8 assert config.embedding_batch_size == 32 assert config.stream_interval == 1 assert config.enable_thinking is None def test_custom_values(self): """Test custom configuration values.""" config = SchedulerConfig( max_num_seqs=128, completion_batch_size=16, embedding_batch_size=12, stream_interval=2, enable_thinking=True, ) assert config.max_num_seqs == 128 assert config.completion_batch_size == 16 assert config.embedding_batch_size == 12 assert config.stream_interval == 2 assert config.enable_thinking is True class TestPagedSSDCacheConfig: """Test cases for PagedSSDCacheConfig dataclass.""" def test_default_values(self): """Test default configuration values.""" config = PagedSSDCacheConfig() assert config.enabled is False assert config.cache_dir is None assert config.max_size == "100GB" def test_custom_values(self): """Test custom configuration values.""" config = PagedSSDCacheConfig( enabled=True, cache_dir=Path("/tmp/cache"), max_size="50GB", ) assert config.enabled is True assert config.cache_dir == Path("/tmp/cache") assert config.max_size == "50GB" def test_max_size_bytes_property(self): """Test max_size_bytes property calculation.""" config = PagedSSDCacheConfig(max_size="100GB") assert config.max_size_bytes == 100 * 1024**3 config = PagedSSDCacheConfig(max_size="50MB") assert config.max_size_bytes == 50 * 1024**2 config = PagedSSDCacheConfig(max_size="1TB") assert config.max_size_bytes == 1024**4 class TestMCPConfig: """Test cases for MCPConfig dataclass.""" def test_default_values(self): """Test default configuration values.""" config = MCPConfig() assert config.config_path is None assert config.enabled is False def test_custom_values(self): """Test custom configuration values.""" config = MCPConfig( config_path="/path/to/mcp.json", enabled=True, ) assert config.config_path == "/path/to/mcp.json" assert config.enabled is True class TestOMLXConfig: """Test cases for OMLXConfig dataclass.""" def test_default_values(self): """Test default configuration values.""" config = OMLXConfig() assert isinstance(config.server, ServerConfig) assert isinstance(config.model, ModelConfig) assert isinstance(config.generation, GenerationConfig) assert isinstance(config.scheduler, SchedulerConfig) assert isinstance(config.cache, CacheConfig) assert isinstance(config.paged_ssd_cache, PagedSSDCacheConfig) assert isinstance(config.mcp, MCPConfig) assert config.continuous_batching is False def test_from_env_default(self): """Test from_env with no environment variables.""" with patch.dict(os.environ, {}, clear=True): config = OMLXConfig.from_env() assert config.server.host == "0.0.0.0" assert config.server.port == 8000 # Issue #926: env default must be False to keep RCE surface closed. assert config.model.trust_remote_code is False def test_from_env_with_variables(self): """Test from_env with environment variables set.""" env_vars = { "OMLX_HOST": "127.0.0.1", "OMLX_PORT": "9000", "OMLX_LOG_LEVEL": "debug", "OMLX_MODEL": "test-model", "OMLX_TRUST_REMOTE_CODE": "false", "OMLX_MAX_TOKENS": "4096", "OMLX_TEMPERATURE": "0.5", "OMLX_CONTINUOUS_BATCHING": "true", } with patch.dict(os.environ, env_vars, clear=True): config = OMLXConfig.from_env() assert config.server.host == "127.0.0.1" assert config.server.port == 9000 assert config.server.log_level == "debug" assert config.model.model_name == "test-model" assert config.model.trust_remote_code is False assert config.generation.max_tokens == 4096 assert config.generation.temperature == 0.5 assert config.continuous_batching is True def test_from_env_paged_ssd_cache(self): """Test from_env with paged SSD cache environment variables.""" env_vars = { "OMLX_PAGED_SSD_CACHE_DIR": "/tmp/ssd_cache", "OMLX_PAGED_SSD_CACHE_MAX_SIZE": "50GB", } with patch.dict(os.environ, env_vars, clear=True): config = OMLXConfig.from_env() assert config.paged_ssd_cache.enabled is True assert config.paged_ssd_cache.cache_dir == Path("/tmp/ssd_cache") assert config.paged_ssd_cache.max_size == "50GB" def test_from_env_mcp(self): """Test from_env with MCP environment variables.""" env_vars = { "OMLX_MCP_CONFIG": "/path/to/mcp.json", } with patch.dict(os.environ, env_vars, clear=True): config = OMLXConfig.from_env() assert config.mcp.enabled is True assert config.mcp.config_path == "/path/to/mcp.json" def test_from_cli_args(self): """Test from_cli_args with argparse namespace.""" args = Namespace( host="127.0.0.1", port=9000, log_level="debug", model="test-model", trust_remote_code=False, max_tokens=4096, temperature=0.7, top_p=0.9, top_k=50, continuous_batching=True, paged_ssd_cache_dir=None, paged_ssd_cache_max_size=None, mcp_config=None, ) with patch.dict(os.environ, {}, clear=True): config = OMLXConfig.from_cli_args(args) assert config.server.host == "127.0.0.1" assert config.server.port == 9000 assert config.model.model_name == "test-model" assert config.generation.max_tokens == 4096 assert config.continuous_batching is True def test_from_cli_args_paged_ssd_cache(self): """Test from_cli_args with paged SSD cache arguments.""" args = Namespace( paged_ssd_cache_dir="/tmp/ssd_cache", paged_ssd_cache_max_size="50GB", ) with patch.dict(os.environ, {}, clear=True): config = OMLXConfig.from_cli_args(args) assert config.paged_ssd_cache.enabled is True assert config.paged_ssd_cache.cache_dir == Path("/tmp/ssd_cache") assert config.paged_ssd_cache.max_size == "50GB" def test_from_cli_args_mcp(self): """Test from_cli_args with MCP arguments.""" args = Namespace( mcp_config="/path/to/mcp.json", ) with patch.dict(os.environ, {}, clear=True): config = OMLXConfig.from_cli_args(args) assert config.mcp.enabled is True assert config.mcp.config_path == "/path/to/mcp.json" def test_to_dict(self): """Test to_dict method.""" config = OMLXConfig() result = config.to_dict() assert "server" in result assert "model" in result assert "generation" in result assert "scheduler" in result assert "cache" in result assert "paged_ssd_cache" in result assert "mcp" in result assert "continuous_batching" in result assert result["server"]["host"] == "0.0.0.0" assert result["server"]["port"] == 8000 def test_to_dict_with_paged_ssd_cache_dir(self): """Test to_dict with paged SSD cache directory.""" config = OMLXConfig() config.paged_ssd_cache.cache_dir = Path("/tmp/cache") result = config.to_dict() assert result["paged_ssd_cache"]["cache_dir"] == "/tmp/cache" def test_validate_valid_config(self): """Test validate with valid configuration.""" config = OMLXConfig() errors = config.validate() assert errors == [] def test_validate_invalid_port(self): """Test validate with invalid port.""" config = OMLXConfig() config.server.port = 0 errors = config.validate() assert any("port" in error.lower() for error in errors) config.server.port = 70000 errors = config.validate() assert any("port" in error.lower() for error in errors) def test_validate_invalid_max_tokens(self): """Test validate with invalid max_tokens.""" config = OMLXConfig() config.generation.max_tokens = 0 errors = config.validate() assert any("max_tokens" in error.lower() for error in errors) config.generation.max_tokens = -1 errors = config.validate() assert any("max_tokens" in error.lower() for error in errors) def test_validate_invalid_temperature(self): """Test validate with invalid temperature.""" config = OMLXConfig() config.generation.temperature = -0.1 errors = config.validate() assert any("temperature" in error.lower() for error in errors) config.generation.temperature = 2.5 errors = config.validate() assert any("temperature" in error.lower() for error in errors) def test_validate_invalid_top_p(self): """Test validate with invalid top_p.""" config = OMLXConfig() config.generation.top_p = -0.1 errors = config.validate() assert any("top_p" in error.lower() for error in errors) config.generation.top_p = 1.5 errors = config.validate() assert any("top_p" in error.lower() for error in errors) def test_validate_paged_ssd_cache_no_dir(self): """Test validate with paged SSD cache enabled but no directory.""" config = OMLXConfig() config.paged_ssd_cache.enabled = True config.paged_ssd_cache.cache_dir = None errors = config.validate() assert any("cache_dir" in error.lower() for error in errors) def test_validate_multiple_errors(self): """Test validate with multiple errors.""" config = OMLXConfig() config.server.port = 0 config.generation.max_tokens = -1 config.generation.temperature = -1.0 errors = config.validate() assert len(errors) >= 3