HOS-Model-Optimizer / tests /test_config.py
lxcxjxhx's picture
Upload tests/test_config.py with huggingface_hub
0d61be6 verified
Raw
History Blame Contribute Delete
23.2 kB
"""
配置模块单元测试
测试配置模块的所有功能,包括:
- 配置文件加载
- 配置生成
- 配置验证
- 模板管理
- 配置合并
使用 pytest 框架进行测试。
"""
import os
import sys
import pytest
from pathlib import Path
from unittest.mock import MagicMock, patch, Mock
import tempfile
import json
# 添加项目路径
sys.path.insert(0, str(Path(__file__).parent.parent))
from hos_optimizer.config import (
ConfigError,
ConfigValidationError,
ConfigConflictError,
TemplateNotFoundError,
ConfigManager,
_BUILTIN_TEMPLATES,
_8GB_OPTIMAL_CONFIGS,
)
class TestConfigManager:
"""配置管理器测试"""
def test_config_manager_initialization_default(self):
"""测试配置管理器默认初始化"""
manager = ConfigManager()
assert manager.config_dir is not None
assert manager._custom_templates == {}
def test_config_manager_initialization_custom_dir(self, tmp_dir):
"""测试配置管理器自定义目录初始化"""
config_dir = os.path.join(tmp_dir, "configs")
manager = ConfigManager(config_dir=config_dir)
assert str(manager.config_dir) == config_dir
class TestConfigLoading:
"""配置加载测试"""
def test_load_config_success(self, tmp_dir):
"""测试成功加载配置文件"""
config_path = os.path.join(tmp_dir, "test_config.yaml")
config_data = {
"backend": "vllm",
"model": {
"path": "/tmp/model",
"format": "awq"
},
"inference": {
"max_model_len": 512
}
}
# 创建 YAML 文件
with patch("hos_optimizer.config.yaml") as mock_yaml:
mock_yaml.safe_load.return_value = config_data
manager = ConfigManager()
result = manager.load_config(config_path)
assert result == config_data
def test_load_config_file_not_found(self, tmp_dir):
"""测试加载不存在的配置文件"""
config_path = os.path.join(tmp_dir, "nonexistent.yaml")
manager = ConfigManager()
with pytest.raises(ConfigError) as exc_info:
manager.load_config(config_path)
assert "配置文件不存在" in str(exc_info.value)
def test_load_config_not_a_file(self, tmp_dir):
"""测试加载路径不是文件的情况"""
manager = ConfigManager()
with pytest.raises(ConfigError) as exc_info:
manager.load_config(tmp_dir) # 目录不是文件
assert "路径不是文件" in str(exc_info.value)
def test_load_config_yaml_error(self, tmp_dir):
"""测试 YAML 解析错误"""
config_path = os.path.join(tmp_dir, "invalid.yaml")
with patch("hos_optimizer.config.yaml") as mock_yaml:
mock_yaml.safe_load.side_effect = Exception("YAML parse error")
mock_yaml.YAMLError = Exception
manager = ConfigManager()
with pytest.raises(ConfigError) as exc_info:
manager.load_config(config_path)
assert "YAML 解析失败" in str(exc_info.value)
def test_load_config_empty_file(self, tmp_dir):
"""测试加载空配置文件"""
config_path = os.path.join(tmp_dir, "empty.yaml")
with patch("hos_optimizer.config.yaml") as mock_yaml:
mock_yaml.safe_load.return_value = None
manager = ConfigManager()
result = manager.load_config(config_path)
assert result == {}
def test_load_config_invalid_structure(self, tmp_dir):
"""测试配置文件结构不是字典"""
config_path = os.path.join(tmp_dir, "list_config.yaml")
with patch("hos_optimizer.config.yaml") as mock_yaml:
mock_yaml.safe_load.return_value = [1, 2, 3] # 列表而不是字典
manager = ConfigManager()
with pytest.raises(ConfigError) as exc_info:
manager.load_config(config_path)
assert "配置文件顶层结构必须是字典" in str(exc_info.value)
class TestConfigSaving:
"""配置保存测试"""
def test_save_config_success(self, tmp_dir):
"""测试成功保存配置"""
output_path = os.path.join(tmp_dir, "output.yaml")
config_data = {
"backend": "vllm",
"model": {"path": "/tmp/model"}
}
with patch("hos_optimizer.config.yaml") as mock_yaml:
manager = ConfigManager()
manager.save_config(config_data, output_path)
mock_yaml.dump.assert_called_once()
def test_save_config_creates_directory(self, tmp_dir):
"""测试保存配置时创建目录"""
output_path = os.path.join(tmp_dir, "new_dir", "output.yaml")
config_data = {"backend": "vllm"}
with patch("hos_optimizer.config.yaml"):
manager = ConfigManager()
manager.save_config(config_data, output_path)
# 验证目录被创建
assert os.path.exists(os.path.join(tmp_dir, "new_dir"))
class TestConfigMerging:
"""配置合并测试"""
def test_merge_configs_simple(self):
"""测试简单配置合并"""
base = {"a": 1, "b": 2}
override = {"b": 3, "c": 4}
manager = ConfigManager()
result = manager.merge_configs(base, override)
assert result == {"a": 1, "b": 3, "c": 4}
def test_merge_configs_nested(self):
"""测试嵌套配置合并"""
base = {
"model": {
"path": "/base",
"format": "gguf"
},
"inference": {
"n_ctx": 512
}
}
override = {
"model": {
"path": "/override"
},
"inference": {
"max_model_len": 2048
}
}
manager = ConfigManager()
result = manager.merge_configs(base, override)
assert result["model"]["path"] == "/override"
assert result["model"]["format"] == "gguf"
assert result["inference"]["n_ctx"] == 512
assert result["inference"]["max_model_len"] == 2048
def test_merge_configs_deep_copy(self):
"""测试合并配置深拷贝"""
base = {"model": {"path": "/base"}}
override = {"model": {"format": "gguf"}}
manager = ConfigManager()
result = manager.merge_configs(base, override)
# 修改原始配置不应该影响结果
base["model"]["path"] = "/modified"
assert result["model"]["path"] == "/base"
class TestConfigGeneration:
"""配置生成测试"""
def test_generate_optimal_config_inference_0_8b(self):
"""测试生成 0.8B 模型推理配置"""
manager = ConfigManager()
config = manager.generate_optimal_config(
scenario="inference_0.8b",
model_path="/tmp/model",
vram_gb=8.0
)
assert config["backend"] == "llama-cpp"
assert config["model"]["path"] == "/tmp/model"
assert "inference" in config
def test_generate_optimal_config_inference_7b(self):
"""测试生成 7B 模型推理配置"""
manager = ConfigManager()
config = manager.generate_optimal_config(
scenario="inference_7b",
model_path="/tmp/model",
vram_gb=8.0
)
assert config["backend"] == "llama-cpp"
assert "inference" in config
def test_generate_optimal_config_training_0_8b(self):
"""测试生成 0.8B 模型训练配置"""
manager = ConfigManager()
config = manager.generate_optimal_config(
scenario="training_0.8b",
model_path="/tmp/model",
vram_gb=8.0
)
assert config["method"] == "qlora"
assert "lora" in config
assert "training" in config
def test_generate_optimal_config_invalid_scenario(self):
"""测试生成无效场景配置"""
manager = ConfigManager()
with pytest.raises(ConfigError) as exc_info:
manager.generate_optimal_config(
scenario="invalid_scenario",
model_path="/tmp/model"
)
assert "不支持的场景" in str(exc_info.value)
def test_generate_optimal_config_low_vram(self):
"""测试低 VRAM 场景配置生成"""
manager = ConfigManager()
config = manager.generate_optimal_config(
scenario="inference_7b",
model_path="/tmp/model",
vram_gb=4.0 # 低 VRAM
)
# 应该调整上下文长度
assert config["inference"]["n_ctx"] <= 512
def test_auto_select_scenario_inference_small(self):
"""测试自动选择小模型推理场景"""
manager = ConfigManager()
config = manager.auto_select_scenario(
model_size_b=0.5,
task="inference",
vram_gb=8.0
)
assert config is not None
assert "backend" in config
def test_auto_select_scenario_training_large(self):
"""测试自动选择大模型训练场景"""
manager = ConfigManager()
config = manager.auto_select_scenario(
model_size_b=7.0,
task="training",
vram_gb=8.0
)
assert config is not None
assert "method" in config
def test_auto_select_scenario_model_too_large(self):
"""测试模型过大时自动选择失败"""
manager = ConfigManager()
with pytest.raises(ConfigError) as exc_info:
manager.auto_select_scenario(
model_size_b=13.0, # 超出 8GB VRAM 支持范围
task="inference",
vram_gb=8.0
)
assert "超出 8GB VRAM 场景支持范围" in str(exc_info.value)
def test_auto_select_scenario_invalid_task(self):
"""测试无效任务类型"""
manager = ConfigManager()
with pytest.raises(ConfigError) as exc_info:
manager.auto_select_scenario(
model_size_b=0.5,
task="invalid_task",
vram_gb=8.0
)
assert "不支持的任务类型" in str(exc_info.value)
class TestConfigValidation:
"""配置验证测试"""
def test_validate_config_valid(self):
"""测试有效配置验证"""
config = {
"backend": "vllm",
"model": {
"path": "/tmp/model",
"format": "awq"
},
"sampling": {
"temperature": 0.7,
"top_p": 0.9
},
"inference": {
"gpu_memory_utilization": 0.9,
"max_model_len": 512
}
}
manager = ConfigManager()
issues = manager.validate_config(config)
# 可能有警告但没有错误
assert isinstance(issues, list)
def test_validate_config_fp16_bf16_conflict(self):
"""测试 fp16 和 bf16 冲突"""
config = {
"training": {
"fp16": True,
"bf16": True
}
}
manager = ConfigManager()
issues = manager.validate_config(config)
# 应该检测到冲突
assert any("fp16 和 bf16 不能同时启用" in issue for issue in issues)
def test_validate_config_gguf_with_dtype(self):
"""测试 GGUF 格式设置 dtype"""
config = {
"model": {
"format": "gguf",
"dtype": "float16"
}
}
manager = ConfigManager()
issues = manager.validate_config(config)
# 应该检测到冲突
assert any("GGUF 格式不支持 dtype" in issue for issue in issues)
def test_validate_config_tensor_parallel_too_large(self):
"""测试 tensor_parallel_size 过大"""
config = {
"inference": {
"tensor_parallel_size": 2
}
}
manager = ConfigManager()
issues = manager.validate_config(config)
# 应该检测到冲突
assert any("tensor_parallel_size" in issue for issue in issues)
def test_validate_config_backend_mismatch(self):
"""测试后端不匹配"""
config = {
"backend": "llama-cpp",
"inference": {
"enable_prefix_caching": True
}
}
manager = ConfigManager()
issues = manager.validate_config(config)
# 应该检测到冲突
assert any("llama-cpp 后端不支持" in issue for issue in issues)
def test_validate_config_temperature_out_of_range(self):
"""测试 temperature 超出范围"""
config = {
"sampling": {
"temperature": 3.0 # 超出 [0, 2.0]
}
}
manager = ConfigManager()
issues = manager.validate_config(config)
# 应该检测到范围问题
assert any("temperature" in issue and "超出合理范围" in issue for issue in issues)
def test_validate_config_top_p_out_of_range(self):
"""测试 top_p 超出范围"""
config = {
"sampling": {
"top_p": 1.5 # 超出 [0, 1.0]
}
}
manager = ConfigManager()
issues = manager.validate_config(config)
# 应该检测到范围问题
assert any("top_p" in issue and "超出合理范围" in issue for issue in issues)
def test_validate_config_gpu_memory_utilization_out_of_range(self):
"""测试 gpu_memory_utilization 超出范围"""
config = {
"inference": {
"gpu_memory_utilization": 0.98 # 超出 [0.5, 0.95]
}
}
manager = ConfigManager()
issues = manager.validate_config(config)
# 应该检测到范围问题
assert any("gpu_memory_utilization" in issue for issue in issues)
def test_validate_config_learning_rate_out_of_range(self):
"""测试 learning_rate 超出范围"""
config = {
"training": {
"learning_rate": 0.1 # 超出 (0, 1e-2]
}
}
manager = ConfigManager()
issues = manager.validate_config(config)
# 应该检测到范围问题
assert any("learning_rate" in issue for issue in issues)
def test_validate_config_negative_context_length(self):
"""测试负数上下文长度"""
config = {
"inference": {
"n_ctx": -100
}
}
manager = ConfigManager()
issues = manager.validate_config(config)
# 应该检测到范围问题
assert any("n_ctx" in issue and "必须为正整数" in issue for issue in issues)
def test_validate_config_empty_model_path(self):
"""测试空模型路径"""
config = {
"model": {
"path": ""
}
}
manager = ConfigManager()
issues = manager.validate_config(config)
# 应该检测到必填字段问题
assert any("model.path 未设置" in issue for issue in issues)
def test_validate_and_raise_success(self):
"""测试验证并抛出异常成功"""
config = {
"model": {
"path": "/tmp/model"
}
}
manager = ConfigManager()
# 不应该抛出异常
manager.validate_and_raise(config)
def test_validate_and_raise_conflict(self):
"""测试验证发现冲突时抛出异常"""
config = {
"training": {
"fp16": True,
"bf16": True
}
}
manager = ConfigManager()
with pytest.raises(ConfigConflictError) as exc_info:
manager.validate_and_raise(config)
assert "配置冲突" in str(exc_info.value)
def test_validate_and_raise_validation_error(self):
"""测试验证发现错误时抛出异常"""
config = {
"model": {
"path": ""
},
"sampling": {
"temperature": 3.0
}
}
manager = ConfigManager()
with pytest.raises((ConfigValidationError, ConfigConflictError)):
manager.validate_and_raise(config)
class TestTemplateManagement:
"""模板管理测试"""
def test_list_templates(self):
"""测试列出所有模板"""
manager = ConfigManager()
templates = manager.list_templates()
assert isinstance(templates, list)
assert len(templates) > 0
# 应该包含内置模板
assert "llama_cpp" in templates
assert "vllm" in templates
assert "sglang" in templates
def test_get_template_builtin(self):
"""测试获取内置模板"""
manager = ConfigManager()
template = manager.get_template("llama_cpp")
assert template is not None
assert isinstance(template, dict)
assert "backend" in template
def test_get_template_not_found(self):
"""测试获取不存在的模板"""
manager = ConfigManager()
with pytest.raises(TemplateNotFoundError) as exc_info:
manager.get_template("nonexistent_template")
assert "模板" in str(exc_info.value)
assert "不存在" in str(exc_info.value)
def test_register_template(self):
"""测试注册自定义模板"""
manager = ConfigManager()
custom_template = {
"backend": "custom",
"model": {"path": ""}
}
manager.register_template("custom_template", custom_template)
# 应该能够获取注册的模板
template = manager.get_template("custom_template")
assert template == custom_template
def test_register_template_invalid_type(self):
"""测试注册非字典类型模板"""
manager = ConfigManager()
with pytest.raises(ConfigError) as exc_info:
manager.register_template("invalid", [1, 2, 3])
assert "模板必须是字典类型" in str(exc_info.value)
def test_unregister_template(self):
"""测试注销自定义模板"""
manager = ConfigManager()
# 先注册
custom_template = {"backend": "custom"}
manager.register_template("to_remove", custom_template)
# 再注销
manager.unregister_template("to_remove")
# 应该无法获取
with pytest.raises(TemplateNotFoundError):
manager.get_template("to_remove")
def test_unregister_builtin_template(self):
"""测试注销内置模板失败"""
manager = ConfigManager()
with pytest.raises(ConfigError) as exc_info:
manager.unregister_template("llama_cpp")
assert "不能注销内置模板" in str(exc_info.value)
def test_unregister_nonexistent_template(self):
"""测试注销不存在的模板"""
manager = ConfigManager()
with pytest.raises(TemplateNotFoundError):
manager.unregister_template("nonexistent")
def test_export_template(self, tmp_dir):
"""测试导出模板"""
output_path = os.path.join(tmp_dir, "exported.yaml")
with patch("hos_optimizer.config.yaml"):
manager = ConfigManager()
manager.export_template("llama_cpp", output_path)
# 验证文件被创建
assert os.path.exists(output_path)
def test_load_template_from_file(self, tmp_dir):
"""测试从文件加载模板"""
config_path = os.path.join(tmp_dir, "template.yaml")
template_data = {
"backend": "custom",
"model": {"path": ""}
}
with patch("hos_optimizer.config.yaml") as mock_yaml:
mock_yaml.safe_load.return_value = template_data
manager = ConfigManager()
manager.load_template_from_file("loaded_template", config_path)
# 应该能够获取加载的模板
template = manager.get_template("loaded_template")
assert template == template_data
class TestNestedOperations:
"""嵌套操作测试"""
def test_get_nested_simple(self):
"""测试简单嵌套读取"""
config = {
"model": {
"path": "/tmp/model"
}
}
result = ConfigManager._get_nested(config, "model.path")
assert result == "/tmp/model"
def test_get_nested_deep(self):
"""测试深层嵌套读取"""
config = {
"level1": {
"level2": {
"level3": {
"value": "deep"
}
}
}
}
result = ConfigManager._get_nested(config, "level1.level2.level3.value")
assert result == "deep"
def test_get_nested_not_found(self):
"""测试读取不存在的路径"""
config = {
"model": {
"path": "/tmp/model"
}
}
result = ConfigManager._get_nested(config, "model.nonexistent")
assert result is None
def test_set_nested_simple(self):
"""测试简单嵌套设置"""
config = {
"model": {
"path": "/old"
}
}
ConfigManager._set_nested(config, "model.path", "/new")
assert config["model"]["path"] == "/new"
def test_set_nested_create_intermediate(self):
"""测试设置时创建中间层级"""
config = {}
ConfigManager._set_nested(config, "model.path", "/tmp/model")
assert config["model"]["path"] == "/tmp/model"
def test_set_nested_deep(self):
"""测试深层嵌套设置"""
config = {
"level1": {
"level2": {}
}
}
ConfigManager._set_nested(config, "level1.level2.level3.value", "deep")
assert config["level1"]["level2"]["level3"]["value"] == "deep"
if __name__ == "__main__":
pytest.main([__file__, "-v"])