lxcxjxhx commited on
Commit
0d61be6
·
verified ·
1 Parent(s): e7120d5

Upload tests/test_config.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. tests/test_config.py +760 -0
tests/test_config.py ADDED
@@ -0,0 +1,760 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 配置模块单元测试
3
+
4
+ 测试配置模块的所有功能,包括:
5
+ - 配置文件加载
6
+ - 配置生成
7
+ - 配置验证
8
+ - 模板管理
9
+ - 配置合并
10
+
11
+ 使用 pytest 框架进行测试。
12
+ """
13
+
14
+ import os
15
+ import sys
16
+ import pytest
17
+ from pathlib import Path
18
+ from unittest.mock import MagicMock, patch, Mock
19
+ import tempfile
20
+ import json
21
+
22
+ # 添加项目路径
23
+ sys.path.insert(0, str(Path(__file__).parent.parent))
24
+
25
+ from hos_optimizer.config import (
26
+ ConfigError,
27
+ ConfigValidationError,
28
+ ConfigConflictError,
29
+ TemplateNotFoundError,
30
+ ConfigManager,
31
+ _BUILTIN_TEMPLATES,
32
+ _8GB_OPTIMAL_CONFIGS,
33
+ )
34
+
35
+
36
+ class TestConfigManager:
37
+ """配置管理器测试"""
38
+
39
+ def test_config_manager_initialization_default(self):
40
+ """测试配置管理器默认初始化"""
41
+ manager = ConfigManager()
42
+
43
+ assert manager.config_dir is not None
44
+ assert manager._custom_templates == {}
45
+
46
+ def test_config_manager_initialization_custom_dir(self, tmp_dir):
47
+ """测试配置管理器自定义目录初始化"""
48
+ config_dir = os.path.join(tmp_dir, "configs")
49
+ manager = ConfigManager(config_dir=config_dir)
50
+
51
+ assert str(manager.config_dir) == config_dir
52
+
53
+
54
+ class TestConfigLoading:
55
+ """配置加载测试"""
56
+
57
+ def test_load_config_success(self, tmp_dir):
58
+ """测试成功加载配置文件"""
59
+ config_path = os.path.join(tmp_dir, "test_config.yaml")
60
+ config_data = {
61
+ "backend": "vllm",
62
+ "model": {
63
+ "path": "/tmp/model",
64
+ "format": "awq"
65
+ },
66
+ "inference": {
67
+ "max_model_len": 512
68
+ }
69
+ }
70
+
71
+ # 创建 YAML 文件
72
+ with patch("hos_optimizer.config.yaml") as mock_yaml:
73
+ mock_yaml.safe_load.return_value = config_data
74
+
75
+ manager = ConfigManager()
76
+ result = manager.load_config(config_path)
77
+
78
+ assert result == config_data
79
+
80
+ def test_load_config_file_not_found(self, tmp_dir):
81
+ """测试加载不存在的配置文件"""
82
+ config_path = os.path.join(tmp_dir, "nonexistent.yaml")
83
+
84
+ manager = ConfigManager()
85
+
86
+ with pytest.raises(ConfigError) as exc_info:
87
+ manager.load_config(config_path)
88
+
89
+ assert "配置文件不存在" in str(exc_info.value)
90
+
91
+ def test_load_config_not_a_file(self, tmp_dir):
92
+ """测试加载路径不是文件的情况"""
93
+ manager = ConfigManager()
94
+
95
+ with pytest.raises(ConfigError) as exc_info:
96
+ manager.load_config(tmp_dir) # 目录不是文件
97
+
98
+ assert "路径不是文件" in str(exc_info.value)
99
+
100
+ def test_load_config_yaml_error(self, tmp_dir):
101
+ """测试 YAML 解析错误"""
102
+ config_path = os.path.join(tmp_dir, "invalid.yaml")
103
+
104
+ with patch("hos_optimizer.config.yaml") as mock_yaml:
105
+ mock_yaml.safe_load.side_effect = Exception("YAML parse error")
106
+ mock_yaml.YAMLError = Exception
107
+
108
+ manager = ConfigManager()
109
+
110
+ with pytest.raises(ConfigError) as exc_info:
111
+ manager.load_config(config_path)
112
+
113
+ assert "YAML 解析失败" in str(exc_info.value)
114
+
115
+ def test_load_config_empty_file(self, tmp_dir):
116
+ """测试加载空配置文件"""
117
+ config_path = os.path.join(tmp_dir, "empty.yaml")
118
+
119
+ with patch("hos_optimizer.config.yaml") as mock_yaml:
120
+ mock_yaml.safe_load.return_value = None
121
+
122
+ manager = ConfigManager()
123
+ result = manager.load_config(config_path)
124
+
125
+ assert result == {}
126
+
127
+ def test_load_config_invalid_structure(self, tmp_dir):
128
+ """测试配置文件结构不是字典"""
129
+ config_path = os.path.join(tmp_dir, "list_config.yaml")
130
+
131
+ with patch("hos_optimizer.config.yaml") as mock_yaml:
132
+ mock_yaml.safe_load.return_value = [1, 2, 3] # 列表而不是字典
133
+
134
+ manager = ConfigManager()
135
+
136
+ with pytest.raises(ConfigError) as exc_info:
137
+ manager.load_config(config_path)
138
+
139
+ assert "配置文件顶层结构必须是字典" in str(exc_info.value)
140
+
141
+
142
+ class TestConfigSaving:
143
+ """配置保存测试"""
144
+
145
+ def test_save_config_success(self, tmp_dir):
146
+ """测试成功保存配置"""
147
+ output_path = os.path.join(tmp_dir, "output.yaml")
148
+ config_data = {
149
+ "backend": "vllm",
150
+ "model": {"path": "/tmp/model"}
151
+ }
152
+
153
+ with patch("hos_optimizer.config.yaml") as mock_yaml:
154
+ manager = ConfigManager()
155
+ manager.save_config(config_data, output_path)
156
+
157
+ mock_yaml.dump.assert_called_once()
158
+
159
+ def test_save_config_creates_directory(self, tmp_dir):
160
+ """测试保存配置时创建目录"""
161
+ output_path = os.path.join(tmp_dir, "new_dir", "output.yaml")
162
+ config_data = {"backend": "vllm"}
163
+
164
+ with patch("hos_optimizer.config.yaml"):
165
+ manager = ConfigManager()
166
+ manager.save_config(config_data, output_path)
167
+
168
+ # 验证目录被创建
169
+ assert os.path.exists(os.path.join(tmp_dir, "new_dir"))
170
+
171
+
172
+ class TestConfigMerging:
173
+ """配置合并测试"""
174
+
175
+ def test_merge_configs_simple(self):
176
+ """测试简单配置合并"""
177
+ base = {"a": 1, "b": 2}
178
+ override = {"b": 3, "c": 4}
179
+
180
+ manager = ConfigManager()
181
+ result = manager.merge_configs(base, override)
182
+
183
+ assert result == {"a": 1, "b": 3, "c": 4}
184
+
185
+ def test_merge_configs_nested(self):
186
+ """测试嵌套配置合并"""
187
+ base = {
188
+ "model": {
189
+ "path": "/base",
190
+ "format": "gguf"
191
+ },
192
+ "inference": {
193
+ "n_ctx": 512
194
+ }
195
+ }
196
+ override = {
197
+ "model": {
198
+ "path": "/override"
199
+ },
200
+ "inference": {
201
+ "max_model_len": 2048
202
+ }
203
+ }
204
+
205
+ manager = ConfigManager()
206
+ result = manager.merge_configs(base, override)
207
+
208
+ assert result["model"]["path"] == "/override"
209
+ assert result["model"]["format"] == "gguf"
210
+ assert result["inference"]["n_ctx"] == 512
211
+ assert result["inference"]["max_model_len"] == 2048
212
+
213
+ def test_merge_configs_deep_copy(self):
214
+ """测试合并配置深拷贝"""
215
+ base = {"model": {"path": "/base"}}
216
+ override = {"model": {"format": "gguf"}}
217
+
218
+ manager = ConfigManager()
219
+ result = manager.merge_configs(base, override)
220
+
221
+ # 修改原始配置不应该影响结果
222
+ base["model"]["path"] = "/modified"
223
+ assert result["model"]["path"] == "/base"
224
+
225
+
226
+ class TestConfigGeneration:
227
+ """配置生成测试"""
228
+
229
+ def test_generate_optimal_config_inference_0_8b(self):
230
+ """测试生成 0.8B 模型推理配置"""
231
+ manager = ConfigManager()
232
+
233
+ config = manager.generate_optimal_config(
234
+ scenario="inference_0.8b",
235
+ model_path="/tmp/model",
236
+ vram_gb=8.0
237
+ )
238
+
239
+ assert config["backend"] == "llama-cpp"
240
+ assert config["model"]["path"] == "/tmp/model"
241
+ assert "inference" in config
242
+
243
+ def test_generate_optimal_config_inference_7b(self):
244
+ """测试生成 7B 模型推理配置"""
245
+ manager = ConfigManager()
246
+
247
+ config = manager.generate_optimal_config(
248
+ scenario="inference_7b",
249
+ model_path="/tmp/model",
250
+ vram_gb=8.0
251
+ )
252
+
253
+ assert config["backend"] == "llama-cpp"
254
+ assert "inference" in config
255
+
256
+ def test_generate_optimal_config_training_0_8b(self):
257
+ """测试生成 0.8B 模型训练配置"""
258
+ manager = ConfigManager()
259
+
260
+ config = manager.generate_optimal_config(
261
+ scenario="training_0.8b",
262
+ model_path="/tmp/model",
263
+ vram_gb=8.0
264
+ )
265
+
266
+ assert config["method"] == "qlora"
267
+ assert "lora" in config
268
+ assert "training" in config
269
+
270
+ def test_generate_optimal_config_invalid_scenario(self):
271
+ """测试生成无效场景配置"""
272
+ manager = ConfigManager()
273
+
274
+ with pytest.raises(ConfigError) as exc_info:
275
+ manager.generate_optimal_config(
276
+ scenario="invalid_scenario",
277
+ model_path="/tmp/model"
278
+ )
279
+
280
+ assert "不支持的场景" in str(exc_info.value)
281
+
282
+ def test_generate_optimal_config_low_vram(self):
283
+ """测试低 VRAM 场景配置生成"""
284
+ manager = ConfigManager()
285
+
286
+ config = manager.generate_optimal_config(
287
+ scenario="inference_7b",
288
+ model_path="/tmp/model",
289
+ vram_gb=4.0 # 低 VRAM
290
+ )
291
+
292
+ # 应该调整上下文长度
293
+ assert config["inference"]["n_ctx"] <= 512
294
+
295
+ def test_auto_select_scenario_inference_small(self):
296
+ """测试自动选择小模型推理场景"""
297
+ manager = ConfigManager()
298
+
299
+ config = manager.auto_select_scenario(
300
+ model_size_b=0.5,
301
+ task="inference",
302
+ vram_gb=8.0
303
+ )
304
+
305
+ assert config is not None
306
+ assert "backend" in config
307
+
308
+ def test_auto_select_scenario_training_large(self):
309
+ """测试自动选择大模型训练场景"""
310
+ manager = ConfigManager()
311
+
312
+ config = manager.auto_select_scenario(
313
+ model_size_b=7.0,
314
+ task="training",
315
+ vram_gb=8.0
316
+ )
317
+
318
+ assert config is not None
319
+ assert "method" in config
320
+
321
+ def test_auto_select_scenario_model_too_large(self):
322
+ """测试模型过大时自动选择失败"""
323
+ manager = ConfigManager()
324
+
325
+ with pytest.raises(ConfigError) as exc_info:
326
+ manager.auto_select_scenario(
327
+ model_size_b=13.0, # 超出 8GB VRAM 支持范围
328
+ task="inference",
329
+ vram_gb=8.0
330
+ )
331
+
332
+ assert "超出 8GB VRAM 场景支持范围" in str(exc_info.value)
333
+
334
+ def test_auto_select_scenario_invalid_task(self):
335
+ """测试无效任务类型"""
336
+ manager = ConfigManager()
337
+
338
+ with pytest.raises(ConfigError) as exc_info:
339
+ manager.auto_select_scenario(
340
+ model_size_b=0.5,
341
+ task="invalid_task",
342
+ vram_gb=8.0
343
+ )
344
+
345
+ assert "不支持的任务类型" in str(exc_info.value)
346
+
347
+
348
+ class TestConfigValidation:
349
+ """配置验证测试"""
350
+
351
+ def test_validate_config_valid(self):
352
+ """测试有效配置验证"""
353
+ config = {
354
+ "backend": "vllm",
355
+ "model": {
356
+ "path": "/tmp/model",
357
+ "format": "awq"
358
+ },
359
+ "sampling": {
360
+ "temperature": 0.7,
361
+ "top_p": 0.9
362
+ },
363
+ "inference": {
364
+ "gpu_memory_utilization": 0.9,
365
+ "max_model_len": 512
366
+ }
367
+ }
368
+
369
+ manager = ConfigManager()
370
+ issues = manager.validate_config(config)
371
+
372
+ # 可能有警告但没有错误
373
+ assert isinstance(issues, list)
374
+
375
+ def test_validate_config_fp16_bf16_conflict(self):
376
+ """测试 fp16 和 bf16 冲突"""
377
+ config = {
378
+ "training": {
379
+ "fp16": True,
380
+ "bf16": True
381
+ }
382
+ }
383
+
384
+ manager = ConfigManager()
385
+ issues = manager.validate_config(config)
386
+
387
+ # 应该检测到冲突
388
+ assert any("fp16 和 bf16 不能同时启用" in issue for issue in issues)
389
+
390
+ def test_validate_config_gguf_with_dtype(self):
391
+ """测试 GGUF 格式设置 dtype"""
392
+ config = {
393
+ "model": {
394
+ "format": "gguf",
395
+ "dtype": "float16"
396
+ }
397
+ }
398
+
399
+ manager = ConfigManager()
400
+ issues = manager.validate_config(config)
401
+
402
+ # 应该检测到冲突
403
+ assert any("GGUF 格式不支持 dtype" in issue for issue in issues)
404
+
405
+ def test_validate_config_tensor_parallel_too_large(self):
406
+ """测试 tensor_parallel_size 过大"""
407
+ config = {
408
+ "inference": {
409
+ "tensor_parallel_size": 2
410
+ }
411
+ }
412
+
413
+ manager = ConfigManager()
414
+ issues = manager.validate_config(config)
415
+
416
+ # 应该检测到冲突
417
+ assert any("tensor_parallel_size" in issue for issue in issues)
418
+
419
+ def test_validate_config_backend_mismatch(self):
420
+ """测试后端不匹配"""
421
+ config = {
422
+ "backend": "llama-cpp",
423
+ "inference": {
424
+ "enable_prefix_caching": True
425
+ }
426
+ }
427
+
428
+ manager = ConfigManager()
429
+ issues = manager.validate_config(config)
430
+
431
+ # 应该检测到冲突
432
+ assert any("llama-cpp 后端不支持" in issue for issue in issues)
433
+
434
+ def test_validate_config_temperature_out_of_range(self):
435
+ """测试 temperature 超出范围"""
436
+ config = {
437
+ "sampling": {
438
+ "temperature": 3.0 # 超出 [0, 2.0]
439
+ }
440
+ }
441
+
442
+ manager = ConfigManager()
443
+ issues = manager.validate_config(config)
444
+
445
+ # 应该检测到范围问题
446
+ assert any("temperature" in issue and "超出合理范围" in issue for issue in issues)
447
+
448
+ def test_validate_config_top_p_out_of_range(self):
449
+ """测试 top_p 超出范围"""
450
+ config = {
451
+ "sampling": {
452
+ "top_p": 1.5 # 超出 [0, 1.0]
453
+ }
454
+ }
455
+
456
+ manager = ConfigManager()
457
+ issues = manager.validate_config(config)
458
+
459
+ # 应该检测到范围问题
460
+ assert any("top_p" in issue and "超出合理范围" in issue for issue in issues)
461
+
462
+ def test_validate_config_gpu_memory_utilization_out_of_range(self):
463
+ """测试 gpu_memory_utilization 超出范围"""
464
+ config = {
465
+ "inference": {
466
+ "gpu_memory_utilization": 0.98 # 超出 [0.5, 0.95]
467
+ }
468
+ }
469
+
470
+ manager = ConfigManager()
471
+ issues = manager.validate_config(config)
472
+
473
+ # 应该检测到范围问题
474
+ assert any("gpu_memory_utilization" in issue for issue in issues)
475
+
476
+ def test_validate_config_learning_rate_out_of_range(self):
477
+ """测试 learning_rate 超出范围"""
478
+ config = {
479
+ "training": {
480
+ "learning_rate": 0.1 # 超出 (0, 1e-2]
481
+ }
482
+ }
483
+
484
+ manager = ConfigManager()
485
+ issues = manager.validate_config(config)
486
+
487
+ # 应该检测到范围问题
488
+ assert any("learning_rate" in issue for issue in issues)
489
+
490
+ def test_validate_config_negative_context_length(self):
491
+ """测试负数上下文长度"""
492
+ config = {
493
+ "inference": {
494
+ "n_ctx": -100
495
+ }
496
+ }
497
+
498
+ manager = ConfigManager()
499
+ issues = manager.validate_config(config)
500
+
501
+ # 应该检测到范围问题
502
+ assert any("n_ctx" in issue and "必须为正整数" in issue for issue in issues)
503
+
504
+ def test_validate_config_empty_model_path(self):
505
+ """测试空模型路径"""
506
+ config = {
507
+ "model": {
508
+ "path": ""
509
+ }
510
+ }
511
+
512
+ manager = ConfigManager()
513
+ issues = manager.validate_config(config)
514
+
515
+ # 应该检测到必填字段问题
516
+ assert any("model.path 未设置" in issue for issue in issues)
517
+
518
+ def test_validate_and_raise_success(self):
519
+ """测试验证并抛出异常成功"""
520
+ config = {
521
+ "model": {
522
+ "path": "/tmp/model"
523
+ }
524
+ }
525
+
526
+ manager = ConfigManager()
527
+ # 不应该抛出异常
528
+ manager.validate_and_raise(config)
529
+
530
+ def test_validate_and_raise_conflict(self):
531
+ """测试验证发现冲突时抛出异常"""
532
+ config = {
533
+ "training": {
534
+ "fp16": True,
535
+ "bf16": True
536
+ }
537
+ }
538
+
539
+ manager = ConfigManager()
540
+
541
+ with pytest.raises(ConfigConflictError) as exc_info:
542
+ manager.validate_and_raise(config)
543
+
544
+ assert "配置冲突" in str(exc_info.value)
545
+
546
+ def test_validate_and_raise_validation_error(self):
547
+ """测试验证发现错误时抛出异常"""
548
+ config = {
549
+ "model": {
550
+ "path": ""
551
+ },
552
+ "sampling": {
553
+ "temperature": 3.0
554
+ }
555
+ }
556
+
557
+ manager = ConfigManager()
558
+
559
+ with pytest.raises((ConfigValidationError, ConfigConflictError)):
560
+ manager.validate_and_raise(config)
561
+
562
+
563
+ class TestTemplateManagement:
564
+ """模板管理测试"""
565
+
566
+ def test_list_templates(self):
567
+ """测试列出所有模板"""
568
+ manager = ConfigManager()
569
+ templates = manager.list_templates()
570
+
571
+ assert isinstance(templates, list)
572
+ assert len(templates) > 0
573
+ # 应该包含内置模板
574
+ assert "llama_cpp" in templates
575
+ assert "vllm" in templates
576
+ assert "sglang" in templates
577
+
578
+ def test_get_template_builtin(self):
579
+ """测试获取内置模板"""
580
+ manager = ConfigManager()
581
+
582
+ template = manager.get_template("llama_cpp")
583
+
584
+ assert template is not None
585
+ assert isinstance(template, dict)
586
+ assert "backend" in template
587
+
588
+ def test_get_template_not_found(self):
589
+ """测试获取不存在的模板"""
590
+ manager = ConfigManager()
591
+
592
+ with pytest.raises(TemplateNotFoundError) as exc_info:
593
+ manager.get_template("nonexistent_template")
594
+
595
+ assert "模板" in str(exc_info.value)
596
+ assert "不存在" in str(exc_info.value)
597
+
598
+ def test_register_template(self):
599
+ """测试注册自定义模板"""
600
+ manager = ConfigManager()
601
+
602
+ custom_template = {
603
+ "backend": "custom",
604
+ "model": {"path": ""}
605
+ }
606
+
607
+ manager.register_template("custom_template", custom_template)
608
+
609
+ # 应该能够获取注册的模板
610
+ template = manager.get_template("custom_template")
611
+ assert template == custom_template
612
+
613
+ def test_register_template_invalid_type(self):
614
+ """测试注册非字典类型模板"""
615
+ manager = ConfigManager()
616
+
617
+ with pytest.raises(ConfigError) as exc_info:
618
+ manager.register_template("invalid", [1, 2, 3])
619
+
620
+ assert "模板必须是字典类型" in str(exc_info.value)
621
+
622
+ def test_unregister_template(self):
623
+ """测试注销自定义模板"""
624
+ manager = ConfigManager()
625
+
626
+ # 先注册
627
+ custom_template = {"backend": "custom"}
628
+ manager.register_template("to_remove", custom_template)
629
+
630
+ # 再注销
631
+ manager.unregister_template("to_remove")
632
+
633
+ # 应该无法获取
634
+ with pytest.raises(TemplateNotFoundError):
635
+ manager.get_template("to_remove")
636
+
637
+ def test_unregister_builtin_template(self):
638
+ """测试注销内置模板失败"""
639
+ manager = ConfigManager()
640
+
641
+ with pytest.raises(ConfigError) as exc_info:
642
+ manager.unregister_template("llama_cpp")
643
+
644
+ assert "不能注销内���模板" in str(exc_info.value)
645
+
646
+ def test_unregister_nonexistent_template(self):
647
+ """测试注销不存在的模板"""
648
+ manager = ConfigManager()
649
+
650
+ with pytest.raises(TemplateNotFoundError):
651
+ manager.unregister_template("nonexistent")
652
+
653
+ def test_export_template(self, tmp_dir):
654
+ """测试导出模板"""
655
+ output_path = os.path.join(tmp_dir, "exported.yaml")
656
+
657
+ with patch("hos_optimizer.config.yaml"):
658
+ manager = ConfigManager()
659
+ manager.export_template("llama_cpp", output_path)
660
+
661
+ # 验证文件被创建
662
+ assert os.path.exists(output_path)
663
+
664
+ def test_load_template_from_file(self, tmp_dir):
665
+ """测试从文件加载模板"""
666
+ config_path = os.path.join(tmp_dir, "template.yaml")
667
+ template_data = {
668
+ "backend": "custom",
669
+ "model": {"path": ""}
670
+ }
671
+
672
+ with patch("hos_optimizer.config.yaml") as mock_yaml:
673
+ mock_yaml.safe_load.return_value = template_data
674
+
675
+ manager = ConfigManager()
676
+ manager.load_template_from_file("loaded_template", config_path)
677
+
678
+ # 应该能够获取加载的模板
679
+ template = manager.get_template("loaded_template")
680
+ assert template == template_data
681
+
682
+
683
+ class TestNestedOperations:
684
+ """嵌套操作测试"""
685
+
686
+ def test_get_nested_simple(self):
687
+ """测试简单嵌套读取"""
688
+ config = {
689
+ "model": {
690
+ "path": "/tmp/model"
691
+ }
692
+ }
693
+
694
+ result = ConfigManager._get_nested(config, "model.path")
695
+
696
+ assert result == "/tmp/model"
697
+
698
+ def test_get_nested_deep(self):
699
+ """测试深层嵌套读取"""
700
+ config = {
701
+ "level1": {
702
+ "level2": {
703
+ "level3": {
704
+ "value": "deep"
705
+ }
706
+ }
707
+ }
708
+ }
709
+
710
+ result = ConfigManager._get_nested(config, "level1.level2.level3.value")
711
+
712
+ assert result == "deep"
713
+
714
+ def test_get_nested_not_found(self):
715
+ """测试读取不存在的路径"""
716
+ config = {
717
+ "model": {
718
+ "path": "/tmp/model"
719
+ }
720
+ }
721
+
722
+ result = ConfigManager._get_nested(config, "model.nonexistent")
723
+
724
+ assert result is None
725
+
726
+ def test_set_nested_simple(self):
727
+ """测试简单嵌套设置"""
728
+ config = {
729
+ "model": {
730
+ "path": "/old"
731
+ }
732
+ }
733
+
734
+ ConfigManager._set_nested(config, "model.path", "/new")
735
+
736
+ assert config["model"]["path"] == "/new"
737
+
738
+ def test_set_nested_create_intermediate(self):
739
+ """测试设置时创建中间层级"""
740
+ config = {}
741
+
742
+ ConfigManager._set_nested(config, "model.path", "/tmp/model")
743
+
744
+ assert config["model"]["path"] == "/tmp/model"
745
+
746
+ def test_set_nested_deep(self):
747
+ """测试深层嵌套设置"""
748
+ config = {
749
+ "level1": {
750
+ "level2": {}
751
+ }
752
+ }
753
+
754
+ ConfigManager._set_nested(config, "level1.level2.level3.value", "deep")
755
+
756
+ assert config["level1"]["level2"]["level3"]["value"] == "deep"
757
+
758
+
759
+ if __name__ == "__main__":
760
+ pytest.main([__file__, "-v"])