mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 21:22:11 -03:00
feat(cache): add cache health monitoring and validation system, see #730
- Add cache entry validator service for data integrity checks - Add cache health monitor service for periodic health checks - Enhance model cache and scanner with validation support - Update websocket manager for health status broadcasting - Add initialization banner service for cache health alerts - Add comprehensive test coverage for new services - Update translations across all locales - Refactor sync translation keys script
This commit is contained in:
283
tests/services/test_cache_entry_validator.py
Normal file
283
tests/services/test_cache_entry_validator.py
Normal file
@@ -0,0 +1,283 @@
|
||||
"""
|
||||
Unit tests for CacheEntryValidator
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from py.services.cache_entry_validator import (
|
||||
CacheEntryValidator,
|
||||
ValidationResult,
|
||||
)
|
||||
|
||||
|
||||
class TestCacheEntryValidator:
|
||||
"""Tests for CacheEntryValidator class"""
|
||||
|
||||
def test_validate_valid_entry(self):
|
||||
"""Test validation of a valid cache entry"""
|
||||
entry = {
|
||||
'file_path': '/models/test.safetensors',
|
||||
'sha256': 'abc123def456',
|
||||
'file_name': 'test.safetensors',
|
||||
'model_name': 'Test Model',
|
||||
'size': 1024,
|
||||
'modified': 1234567890.0,
|
||||
'tags': ['tag1', 'tag2'],
|
||||
}
|
||||
|
||||
result = CacheEntryValidator.validate(entry, auto_repair=False)
|
||||
|
||||
assert result.is_valid is True
|
||||
assert result.repaired is False
|
||||
assert len(result.errors) == 0
|
||||
assert result.entry == entry
|
||||
|
||||
def test_validate_missing_required_field_sha256(self):
|
||||
"""Test validation fails when required sha256 field is missing"""
|
||||
entry = {
|
||||
'file_path': '/models/test.safetensors',
|
||||
# sha256 missing
|
||||
'file_name': 'test.safetensors',
|
||||
}
|
||||
|
||||
result = CacheEntryValidator.validate(entry, auto_repair=False)
|
||||
|
||||
assert result.is_valid is False
|
||||
assert result.repaired is False
|
||||
assert any('sha256' in error for error in result.errors)
|
||||
|
||||
def test_validate_missing_required_field_file_path(self):
|
||||
"""Test validation fails when required file_path field is missing"""
|
||||
entry = {
|
||||
# file_path missing
|
||||
'sha256': 'abc123def456',
|
||||
'file_name': 'test.safetensors',
|
||||
}
|
||||
|
||||
result = CacheEntryValidator.validate(entry, auto_repair=False)
|
||||
|
||||
assert result.is_valid is False
|
||||
assert result.repaired is False
|
||||
assert any('file_path' in error for error in result.errors)
|
||||
|
||||
def test_validate_empty_required_field_sha256(self):
|
||||
"""Test validation fails when sha256 is empty string"""
|
||||
entry = {
|
||||
'file_path': '/models/test.safetensors',
|
||||
'sha256': '', # Empty string
|
||||
}
|
||||
|
||||
result = CacheEntryValidator.validate(entry, auto_repair=False)
|
||||
|
||||
assert result.is_valid is False
|
||||
assert result.repaired is False
|
||||
assert any('sha256' in error for error in result.errors)
|
||||
|
||||
def test_validate_empty_required_field_file_path(self):
|
||||
"""Test validation fails when file_path is empty string"""
|
||||
entry = {
|
||||
'file_path': '', # Empty string
|
||||
'sha256': 'abc123def456',
|
||||
}
|
||||
|
||||
result = CacheEntryValidator.validate(entry, auto_repair=False)
|
||||
|
||||
assert result.is_valid is False
|
||||
assert result.repaired is False
|
||||
assert any('file_path' in error for error in result.errors)
|
||||
|
||||
def test_validate_none_required_field(self):
|
||||
"""Test validation fails when required field is None"""
|
||||
entry = {
|
||||
'file_path': None,
|
||||
'sha256': 'abc123def456',
|
||||
}
|
||||
|
||||
result = CacheEntryValidator.validate(entry, auto_repair=False)
|
||||
|
||||
assert result.is_valid is False
|
||||
assert result.repaired is False
|
||||
assert any('file_path' in error for error in result.errors)
|
||||
|
||||
def test_validate_none_entry(self):
|
||||
"""Test validation handles None entry"""
|
||||
result = CacheEntryValidator.validate(None, auto_repair=False)
|
||||
|
||||
assert result.is_valid is False
|
||||
assert result.repaired is False
|
||||
assert any('None' in error for error in result.errors)
|
||||
assert result.entry is None
|
||||
|
||||
def test_validate_non_dict_entry(self):
|
||||
"""Test validation handles non-dict entry"""
|
||||
result = CacheEntryValidator.validate("not a dict", auto_repair=False)
|
||||
|
||||
assert result.is_valid is False
|
||||
assert result.repaired is False
|
||||
assert any('not a dict' in error for error in result.errors)
|
||||
assert result.entry is None
|
||||
|
||||
def test_auto_repair_missing_non_required_field(self):
|
||||
"""Test auto-repair adds missing non-required fields"""
|
||||
entry = {
|
||||
'file_path': '/models/test.safetensors',
|
||||
'sha256': 'abc123def456',
|
||||
# file_name, model_name, tags missing
|
||||
}
|
||||
|
||||
result = CacheEntryValidator.validate(entry, auto_repair=True)
|
||||
|
||||
assert result.is_valid is True
|
||||
assert result.repaired is True
|
||||
assert result.entry['file_name'] == ''
|
||||
assert result.entry['model_name'] == ''
|
||||
assert result.entry['tags'] == []
|
||||
|
||||
def test_auto_repair_wrong_type_field(self):
|
||||
"""Test auto-repair fixes fields with wrong type"""
|
||||
entry = {
|
||||
'file_path': '/models/test.safetensors',
|
||||
'sha256': 'abc123def456',
|
||||
'size': 'not a number', # Should be int
|
||||
'tags': 'not a list', # Should be list
|
||||
}
|
||||
|
||||
result = CacheEntryValidator.validate(entry, auto_repair=True)
|
||||
|
||||
assert result.is_valid is True
|
||||
assert result.repaired is True
|
||||
assert result.entry['size'] == 0 # Default value
|
||||
assert result.entry['tags'] == [] # Default value
|
||||
|
||||
def test_normalize_sha256_lowercase(self):
|
||||
"""Test sha256 is normalized to lowercase"""
|
||||
entry = {
|
||||
'file_path': '/models/test.safetensors',
|
||||
'sha256': 'ABC123DEF456', # Uppercase
|
||||
}
|
||||
|
||||
result = CacheEntryValidator.validate(entry, auto_repair=True)
|
||||
|
||||
assert result.is_valid is True
|
||||
assert result.entry['sha256'] == 'abc123def456'
|
||||
|
||||
def test_validate_batch_all_valid(self):
|
||||
"""Test batch validation with all valid entries"""
|
||||
entries = [
|
||||
{
|
||||
'file_path': '/models/test1.safetensors',
|
||||
'sha256': 'abc123',
|
||||
},
|
||||
{
|
||||
'file_path': '/models/test2.safetensors',
|
||||
'sha256': 'def456',
|
||||
},
|
||||
]
|
||||
|
||||
valid, invalid = CacheEntryValidator.validate_batch(entries, auto_repair=False)
|
||||
|
||||
assert len(valid) == 2
|
||||
assert len(invalid) == 0
|
||||
|
||||
def test_validate_batch_mixed_validity(self):
|
||||
"""Test batch validation with mixed valid/invalid entries"""
|
||||
entries = [
|
||||
{
|
||||
'file_path': '/models/test1.safetensors',
|
||||
'sha256': 'abc123',
|
||||
},
|
||||
{
|
||||
'file_path': '/models/test2.safetensors',
|
||||
# sha256 missing - invalid
|
||||
},
|
||||
{
|
||||
'file_path': '/models/test3.safetensors',
|
||||
'sha256': 'def456',
|
||||
},
|
||||
]
|
||||
|
||||
valid, invalid = CacheEntryValidator.validate_batch(entries, auto_repair=False)
|
||||
|
||||
assert len(valid) == 2
|
||||
assert len(invalid) == 1
|
||||
# invalid list contains the actual invalid entries (not by index)
|
||||
assert invalid[0]['file_path'] == '/models/test2.safetensors'
|
||||
|
||||
def test_validate_batch_empty_list(self):
|
||||
"""Test batch validation with empty list"""
|
||||
valid, invalid = CacheEntryValidator.validate_batch([], auto_repair=False)
|
||||
|
||||
assert len(valid) == 0
|
||||
assert len(invalid) == 0
|
||||
|
||||
def test_get_file_path_safe(self):
|
||||
"""Test safe file_path extraction"""
|
||||
entry = {'file_path': '/models/test.safetensors', 'sha256': 'abc123'}
|
||||
assert CacheEntryValidator.get_file_path_safe(entry) == '/models/test.safetensors'
|
||||
|
||||
def test_get_file_path_safe_missing(self):
|
||||
"""Test safe file_path extraction when missing"""
|
||||
entry = {'sha256': 'abc123'}
|
||||
assert CacheEntryValidator.get_file_path_safe(entry) == ''
|
||||
|
||||
def test_get_file_path_safe_not_dict(self):
|
||||
"""Test safe file_path extraction from non-dict"""
|
||||
assert CacheEntryValidator.get_file_path_safe(None) == ''
|
||||
assert CacheEntryValidator.get_file_path_safe('string') == ''
|
||||
|
||||
def test_get_sha256_safe(self):
|
||||
"""Test safe sha256 extraction"""
|
||||
entry = {'file_path': '/models/test.safetensors', 'sha256': 'ABC123'}
|
||||
assert CacheEntryValidator.get_sha256_safe(entry) == 'abc123'
|
||||
|
||||
def test_get_sha256_safe_missing(self):
|
||||
"""Test safe sha256 extraction when missing"""
|
||||
entry = {'file_path': '/models/test.safetensors'}
|
||||
assert CacheEntryValidator.get_sha256_safe(entry) == ''
|
||||
|
||||
def test_get_sha256_safe_not_dict(self):
|
||||
"""Test safe sha256 extraction from non-dict"""
|
||||
assert CacheEntryValidator.get_sha256_safe(None) == ''
|
||||
assert CacheEntryValidator.get_sha256_safe('string') == ''
|
||||
|
||||
def test_validate_with_all_optional_fields(self):
|
||||
"""Test validation with all optional fields present"""
|
||||
entry = {
|
||||
'file_path': '/models/test.safetensors',
|
||||
'sha256': 'abc123',
|
||||
'file_name': 'test.safetensors',
|
||||
'model_name': 'Test Model',
|
||||
'folder': 'test_folder',
|
||||
'size': 1024,
|
||||
'modified': 1234567890.0,
|
||||
'tags': ['tag1', 'tag2'],
|
||||
'preview_url': 'http://example.com/preview.jpg',
|
||||
'base_model': 'SD1.5',
|
||||
'from_civitai': True,
|
||||
'favorite': True,
|
||||
'exclude': False,
|
||||
'db_checked': True,
|
||||
'preview_nsfw_level': 1,
|
||||
'notes': 'Test notes',
|
||||
'usage_tips': 'Test tips',
|
||||
}
|
||||
|
||||
result = CacheEntryValidator.validate(entry, auto_repair=False)
|
||||
|
||||
assert result.is_valid is True
|
||||
assert result.repaired is False
|
||||
assert result.entry == entry
|
||||
|
||||
def test_validate_numeric_field_accepts_float_for_int(self):
|
||||
"""Test that numeric fields accept float for int type"""
|
||||
entry = {
|
||||
'file_path': '/models/test.safetensors',
|
||||
'sha256': 'abc123',
|
||||
'size': 1024.5, # Float for int field
|
||||
'modified': 1234567890.0,
|
||||
}
|
||||
|
||||
result = CacheEntryValidator.validate(entry, auto_repair=False)
|
||||
|
||||
assert result.is_valid is True
|
||||
assert result.repaired is False
|
||||
364
tests/services/test_cache_health_monitor.py
Normal file
364
tests/services/test_cache_health_monitor.py
Normal file
@@ -0,0 +1,364 @@
|
||||
"""
|
||||
Unit tests for CacheHealthMonitor
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from py.services.cache_health_monitor import (
|
||||
CacheHealthMonitor,
|
||||
CacheHealthStatus,
|
||||
HealthReport,
|
||||
)
|
||||
|
||||
|
||||
class TestCacheHealthMonitor:
|
||||
"""Tests for CacheHealthMonitor class"""
|
||||
|
||||
def test_check_health_all_valid_entries(self):
|
||||
"""Test health check with 100% valid entries"""
|
||||
monitor = CacheHealthMonitor()
|
||||
|
||||
entries = [
|
||||
{
|
||||
'file_path': f'/models/test{i}.safetensors',
|
||||
'sha256': f'hash{i}',
|
||||
}
|
||||
for i in range(100)
|
||||
]
|
||||
|
||||
report = monitor.check_health(entries, auto_repair=False)
|
||||
|
||||
assert report.status == CacheHealthStatus.HEALTHY
|
||||
assert report.total_entries == 100
|
||||
assert report.valid_entries == 100
|
||||
assert report.invalid_entries == 0
|
||||
assert report.repaired_entries == 0
|
||||
assert report.corruption_rate == 0.0
|
||||
assert report.message == "Cache is healthy"
|
||||
|
||||
def test_check_health_degraded_cache(self):
|
||||
"""Test health check with 1-5% invalid entries (degraded)"""
|
||||
monitor = CacheHealthMonitor()
|
||||
|
||||
# Create 100 entries, 2 invalid (2%)
|
||||
entries = [
|
||||
{
|
||||
'file_path': f'/models/test{i}.safetensors',
|
||||
'sha256': f'hash{i}',
|
||||
}
|
||||
for i in range(98)
|
||||
]
|
||||
# Add 2 invalid entries
|
||||
entries.append({'file_path': '/models/invalid1.safetensors'}) # Missing sha256
|
||||
entries.append({'file_path': '/models/invalid2.safetensors'}) # Missing sha256
|
||||
|
||||
report = monitor.check_health(entries, auto_repair=False)
|
||||
|
||||
assert report.status == CacheHealthStatus.DEGRADED
|
||||
assert report.total_entries == 100
|
||||
assert report.valid_entries == 98
|
||||
assert report.invalid_entries == 2
|
||||
assert report.corruption_rate == 0.02
|
||||
# Message describes the issue without necessarily containing the word "degraded"
|
||||
assert 'invalid entries' in report.message.lower()
|
||||
|
||||
def test_check_health_corrupted_cache(self):
|
||||
"""Test health check with >5% invalid entries (corrupted)"""
|
||||
monitor = CacheHealthMonitor()
|
||||
|
||||
# Create 100 entries, 10 invalid (10%)
|
||||
entries = [
|
||||
{
|
||||
'file_path': f'/models/test{i}.safetensors',
|
||||
'sha256': f'hash{i}',
|
||||
}
|
||||
for i in range(90)
|
||||
]
|
||||
# Add 10 invalid entries
|
||||
for i in range(10):
|
||||
entries.append({'file_path': f'/models/invalid{i}.safetensors'})
|
||||
|
||||
report = monitor.check_health(entries, auto_repair=False)
|
||||
|
||||
assert report.status == CacheHealthStatus.CORRUPTED
|
||||
assert report.total_entries == 100
|
||||
assert report.valid_entries == 90
|
||||
assert report.invalid_entries == 10
|
||||
assert report.corruption_rate == 0.10
|
||||
assert 'corrupted' in report.message.lower()
|
||||
|
||||
def test_check_health_empty_cache(self):
|
||||
"""Test health check with empty cache"""
|
||||
monitor = CacheHealthMonitor()
|
||||
|
||||
report = monitor.check_health([], auto_repair=False)
|
||||
|
||||
assert report.status == CacheHealthStatus.HEALTHY
|
||||
assert report.total_entries == 0
|
||||
assert report.valid_entries == 0
|
||||
assert report.invalid_entries == 0
|
||||
assert report.corruption_rate == 0.0
|
||||
assert report.message == "Cache is empty"
|
||||
|
||||
def test_check_health_single_invalid_entry(self):
|
||||
"""Test health check with 1 invalid entry out of 1 (100% corruption)"""
|
||||
monitor = CacheHealthMonitor()
|
||||
|
||||
entries = [{'file_path': '/models/invalid.safetensors'}]
|
||||
|
||||
report = monitor.check_health(entries, auto_repair=False)
|
||||
|
||||
assert report.status == CacheHealthStatus.CORRUPTED
|
||||
assert report.total_entries == 1
|
||||
assert report.valid_entries == 0
|
||||
assert report.invalid_entries == 1
|
||||
assert report.corruption_rate == 1.0
|
||||
|
||||
def test_check_health_boundary_degraded_threshold(self):
|
||||
"""Test health check at degraded threshold (1%)"""
|
||||
monitor = CacheHealthMonitor(degraded_threshold=0.01)
|
||||
|
||||
# 100 entries, 1 invalid (exactly 1%)
|
||||
entries = [
|
||||
{
|
||||
'file_path': f'/models/test{i}.safetensors',
|
||||
'sha256': f'hash{i}',
|
||||
}
|
||||
for i in range(99)
|
||||
]
|
||||
entries.append({'file_path': '/models/invalid.safetensors'})
|
||||
|
||||
report = monitor.check_health(entries, auto_repair=False)
|
||||
|
||||
assert report.status == CacheHealthStatus.DEGRADED
|
||||
assert report.corruption_rate == 0.01
|
||||
|
||||
def test_check_health_boundary_corrupted_threshold(self):
|
||||
"""Test health check at corrupted threshold (5%)"""
|
||||
monitor = CacheHealthMonitor(corrupted_threshold=0.05)
|
||||
|
||||
# 100 entries, 5 invalid (exactly 5%)
|
||||
entries = [
|
||||
{
|
||||
'file_path': f'/models/test{i}.safetensors',
|
||||
'sha256': f'hash{i}',
|
||||
}
|
||||
for i in range(95)
|
||||
]
|
||||
for i in range(5):
|
||||
entries.append({'file_path': f'/models/invalid{i}.safetensors'})
|
||||
|
||||
report = monitor.check_health(entries, auto_repair=False)
|
||||
|
||||
assert report.status == CacheHealthStatus.CORRUPTED
|
||||
assert report.corruption_rate == 0.05
|
||||
|
||||
def test_check_health_below_degraded_threshold(self):
|
||||
"""Test health check below degraded threshold (0%)"""
|
||||
monitor = CacheHealthMonitor(degraded_threshold=0.01)
|
||||
|
||||
# All entries valid
|
||||
entries = [
|
||||
{
|
||||
'file_path': f'/models/test{i}.safetensors',
|
||||
'sha256': f'hash{i}',
|
||||
}
|
||||
for i in range(100)
|
||||
]
|
||||
|
||||
report = monitor.check_health(entries, auto_repair=False)
|
||||
|
||||
assert report.status == CacheHealthStatus.HEALTHY
|
||||
assert report.corruption_rate == 0.0
|
||||
|
||||
def test_check_health_auto_repair(self):
|
||||
"""Test health check with auto_repair enabled"""
|
||||
monitor = CacheHealthMonitor()
|
||||
|
||||
# 1 entry with all fields (won't be repaired), 1 entry with missing non-required fields (will be repaired)
|
||||
complete_entry = {
|
||||
'file_path': '/models/test1.safetensors',
|
||||
'sha256': 'hash1',
|
||||
'file_name': 'test1.safetensors',
|
||||
'model_name': 'Model 1',
|
||||
'folder': '',
|
||||
'size': 0,
|
||||
'modified': 0.0,
|
||||
'tags': ['tag1'],
|
||||
'preview_url': '',
|
||||
'base_model': '',
|
||||
'from_civitai': True,
|
||||
'favorite': False,
|
||||
'exclude': False,
|
||||
'db_checked': False,
|
||||
'preview_nsfw_level': 0,
|
||||
'notes': '',
|
||||
'usage_tips': '',
|
||||
}
|
||||
incomplete_entry = {
|
||||
'file_path': '/models/test2.safetensors',
|
||||
'sha256': 'hash2',
|
||||
# Missing many optional fields (will be repaired)
|
||||
}
|
||||
|
||||
entries = [complete_entry, incomplete_entry]
|
||||
|
||||
report = monitor.check_health(entries, auto_repair=True)
|
||||
|
||||
assert report.status == CacheHealthStatus.HEALTHY
|
||||
assert report.total_entries == 2
|
||||
assert report.valid_entries == 2
|
||||
assert report.invalid_entries == 0
|
||||
assert report.repaired_entries == 1
|
||||
|
||||
def test_should_notify_user_healthy(self):
|
||||
"""Test should_notify_user for healthy cache"""
|
||||
monitor = CacheHealthMonitor()
|
||||
|
||||
report = HealthReport(
|
||||
status=CacheHealthStatus.HEALTHY,
|
||||
total_entries=100,
|
||||
valid_entries=100,
|
||||
invalid_entries=0,
|
||||
repaired_entries=0,
|
||||
message="Cache is healthy"
|
||||
)
|
||||
|
||||
assert monitor.should_notify_user(report) is False
|
||||
|
||||
def test_should_notify_user_degraded(self):
|
||||
"""Test should_notify_user for degraded cache"""
|
||||
monitor = CacheHealthMonitor()
|
||||
|
||||
report = HealthReport(
|
||||
status=CacheHealthStatus.DEGRADED,
|
||||
total_entries=100,
|
||||
valid_entries=98,
|
||||
invalid_entries=2,
|
||||
repaired_entries=0,
|
||||
message="Cache is degraded"
|
||||
)
|
||||
|
||||
assert monitor.should_notify_user(report) is True
|
||||
|
||||
def test_should_notify_user_corrupted(self):
|
||||
"""Test should_notify_user for corrupted cache"""
|
||||
monitor = CacheHealthMonitor()
|
||||
|
||||
report = HealthReport(
|
||||
status=CacheHealthStatus.CORRUPTED,
|
||||
total_entries=100,
|
||||
valid_entries=90,
|
||||
invalid_entries=10,
|
||||
repaired_entries=0,
|
||||
message="Cache is corrupted"
|
||||
)
|
||||
|
||||
assert monitor.should_notify_user(report) is True
|
||||
|
||||
def test_get_notification_severity_degraded(self):
|
||||
"""Test get_notification_severity for degraded cache"""
|
||||
monitor = CacheHealthMonitor()
|
||||
|
||||
report = HealthReport(
|
||||
status=CacheHealthStatus.DEGRADED,
|
||||
total_entries=100,
|
||||
valid_entries=98,
|
||||
invalid_entries=2,
|
||||
repaired_entries=0,
|
||||
message="Cache is degraded"
|
||||
)
|
||||
|
||||
assert monitor.get_notification_severity(report) == 'warning'
|
||||
|
||||
def test_get_notification_severity_corrupted(self):
|
||||
"""Test get_notification_severity for corrupted cache"""
|
||||
monitor = CacheHealthMonitor()
|
||||
|
||||
report = HealthReport(
|
||||
status=CacheHealthStatus.CORRUPTED,
|
||||
total_entries=100,
|
||||
valid_entries=90,
|
||||
invalid_entries=10,
|
||||
repaired_entries=0,
|
||||
message="Cache is corrupted"
|
||||
)
|
||||
|
||||
assert monitor.get_notification_severity(report) == 'error'
|
||||
|
||||
def test_report_to_dict(self):
|
||||
"""Test HealthReport to_dict conversion"""
|
||||
report = HealthReport(
|
||||
status=CacheHealthStatus.DEGRADED,
|
||||
total_entries=100,
|
||||
valid_entries=98,
|
||||
invalid_entries=2,
|
||||
repaired_entries=1,
|
||||
invalid_paths=['/path1', '/path2'],
|
||||
message="Cache issues detected"
|
||||
)
|
||||
|
||||
result = report.to_dict()
|
||||
|
||||
assert result['status'] == 'degraded'
|
||||
assert result['total_entries'] == 100
|
||||
assert result['valid_entries'] == 98
|
||||
assert result['invalid_entries'] == 2
|
||||
assert result['repaired_entries'] == 1
|
||||
assert result['corruption_rate'] == '2.0%'
|
||||
assert len(result['invalid_paths']) == 2
|
||||
assert result['message'] == "Cache issues detected"
|
||||
|
||||
def test_report_corruption_rate_zero_division(self):
|
||||
"""Test corruption_rate calculation with zero entries"""
|
||||
report = HealthReport(
|
||||
status=CacheHealthStatus.HEALTHY,
|
||||
total_entries=0,
|
||||
valid_entries=0,
|
||||
invalid_entries=0,
|
||||
repaired_entries=0,
|
||||
message="Cache is empty"
|
||||
)
|
||||
|
||||
assert report.corruption_rate == 0.0
|
||||
|
||||
def test_check_health_collects_invalid_paths(self):
|
||||
"""Test health check collects invalid entry paths"""
|
||||
monitor = CacheHealthMonitor()
|
||||
|
||||
entries = [
|
||||
{
|
||||
'file_path': '/models/valid.safetensors',
|
||||
'sha256': 'hash1',
|
||||
},
|
||||
{
|
||||
'file_path': '/models/invalid1.safetensors',
|
||||
},
|
||||
{
|
||||
'file_path': '/models/invalid2.safetensors',
|
||||
},
|
||||
]
|
||||
|
||||
report = monitor.check_health(entries, auto_repair=False)
|
||||
|
||||
assert len(report.invalid_paths) == 2
|
||||
assert '/models/invalid1.safetensors' in report.invalid_paths
|
||||
assert '/models/invalid2.safetensors' in report.invalid_paths
|
||||
|
||||
def test_report_to_dict_limits_invalid_paths(self):
|
||||
"""Test that to_dict limits invalid_paths to first 10"""
|
||||
report = HealthReport(
|
||||
status=CacheHealthStatus.CORRUPTED,
|
||||
total_entries=15,
|
||||
valid_entries=0,
|
||||
invalid_entries=15,
|
||||
repaired_entries=0,
|
||||
invalid_paths=[f'/path{i}' for i in range(15)],
|
||||
message="Cache corrupted"
|
||||
)
|
||||
|
||||
result = report.to_dict()
|
||||
|
||||
assert len(result['invalid_paths']) == 10
|
||||
assert result['invalid_paths'][0] == '/path0'
|
||||
assert result['invalid_paths'][-1] == '/path9'
|
||||
167
tests/services/test_model_scanner_cache_validation.py
Normal file
167
tests/services/test_model_scanner_cache_validation.py
Normal file
@@ -0,0 +1,167 @@
|
||||
"""
|
||||
Integration tests for cache validation in ModelScanner
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import asyncio
|
||||
|
||||
from py.services.model_scanner import ModelScanner
|
||||
from py.services.cache_entry_validator import CacheEntryValidator
|
||||
from py.services.cache_health_monitor import CacheHealthMonitor, CacheHealthStatus
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_model_scanner_validates_cache_entries(tmp_path_factory):
|
||||
"""Test that ModelScanner validates cache entries during initialization"""
|
||||
# Create temporary test data
|
||||
tmp_dir = tmp_path_factory.mktemp("test_loras")
|
||||
|
||||
# Create test files
|
||||
test_file = tmp_dir / "test_model.safetensors"
|
||||
test_file.write_bytes(b"fake model data" * 100)
|
||||
|
||||
# Mock model scanner (we can't easily instantiate a full scanner in tests)
|
||||
# Instead, test the validation logic directly
|
||||
entries = [
|
||||
{
|
||||
'file_path': str(test_file),
|
||||
'sha256': 'abc123def456',
|
||||
'file_name': 'test_model.safetensors',
|
||||
},
|
||||
{
|
||||
'file_path': str(tmp_dir / 'invalid.safetensors'),
|
||||
# Missing sha256 - invalid
|
||||
},
|
||||
]
|
||||
|
||||
valid, invalid = CacheEntryValidator.validate_batch(entries, auto_repair=True)
|
||||
|
||||
assert len(valid) == 1
|
||||
assert len(invalid) == 1
|
||||
assert valid[0]['sha256'] == 'abc123def456'
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_model_scanner_detects_degraded_cache():
|
||||
"""Test that ModelScanner detects degraded cache health"""
|
||||
# Create 100 entries with 2% corruption
|
||||
entries = [
|
||||
{
|
||||
'file_path': f'/models/test{i}.safetensors',
|
||||
'sha256': f'hash{i}',
|
||||
}
|
||||
for i in range(98)
|
||||
]
|
||||
# Add 2 invalid entries
|
||||
entries.append({'file_path': '/models/invalid1.safetensors'})
|
||||
entries.append({'file_path': '/models/invalid2.safetensors'})
|
||||
|
||||
monitor = CacheHealthMonitor()
|
||||
report = monitor.check_health(entries, auto_repair=True)
|
||||
|
||||
assert report.status == CacheHealthStatus.DEGRADED
|
||||
assert report.invalid_entries == 2
|
||||
assert report.valid_entries == 98
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_model_scanner_detects_corrupted_cache():
|
||||
"""Test that ModelScanner detects corrupted cache health"""
|
||||
# Create 100 entries with 10% corruption
|
||||
entries = [
|
||||
{
|
||||
'file_path': f'/models/test{i}.safetensors',
|
||||
'sha256': f'hash{i}',
|
||||
}
|
||||
for i in range(90)
|
||||
]
|
||||
# Add 10 invalid entries
|
||||
for i in range(10):
|
||||
entries.append({'file_path': f'/models/invalid{i}.safetensors'})
|
||||
|
||||
monitor = CacheHealthMonitor()
|
||||
report = monitor.check_health(entries, auto_repair=True)
|
||||
|
||||
assert report.status == CacheHealthStatus.CORRUPTED
|
||||
assert report.invalid_entries == 10
|
||||
assert report.valid_entries == 90
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_model_scanner_removes_invalid_from_hash_index():
|
||||
"""Test that ModelScanner removes invalid entries from hash index"""
|
||||
from py.services.model_hash_index import ModelHashIndex
|
||||
|
||||
# Create a hash index with some entries
|
||||
hash_index = ModelHashIndex()
|
||||
valid_entry = {
|
||||
'file_path': '/models/valid.safetensors',
|
||||
'sha256': 'abc123',
|
||||
}
|
||||
invalid_entry = {
|
||||
'file_path': '/models/invalid.safetensors',
|
||||
'sha256': '', # Empty sha256
|
||||
}
|
||||
|
||||
# Add entries to hash index
|
||||
hash_index.add_entry(valid_entry['sha256'], valid_entry['file_path'])
|
||||
hash_index.add_entry(invalid_entry['sha256'], invalid_entry['file_path'])
|
||||
|
||||
# Verify both entries are in the index (using get_hash method)
|
||||
assert hash_index.get_hash(valid_entry['file_path']) == valid_entry['sha256']
|
||||
# Invalid entry won't be added due to empty sha256
|
||||
assert hash_index.get_hash(invalid_entry['file_path']) is None
|
||||
|
||||
# Simulate removing invalid entry (it's not actually there, but let's test the method)
|
||||
hash_index.remove_by_path(
|
||||
CacheEntryValidator.get_file_path_safe(invalid_entry),
|
||||
CacheEntryValidator.get_sha256_safe(invalid_entry)
|
||||
)
|
||||
|
||||
# Verify valid entry remains
|
||||
assert hash_index.get_hash(valid_entry['file_path']) == valid_entry['sha256']
|
||||
|
||||
|
||||
def test_cache_entry_validator_handles_various_field_types():
|
||||
"""Test that validator handles various field types correctly"""
|
||||
# Test with different field types
|
||||
entry = {
|
||||
'file_path': '/models/test.safetensors',
|
||||
'sha256': 'abc123',
|
||||
'size': 1024, # int
|
||||
'modified': 1234567890.0, # float
|
||||
'favorite': True, # bool
|
||||
'tags': ['tag1', 'tag2'], # list
|
||||
'exclude': False, # bool
|
||||
}
|
||||
|
||||
result = CacheEntryValidator.validate(entry, auto_repair=False)
|
||||
|
||||
assert result.is_valid is True
|
||||
assert result.repaired is False
|
||||
|
||||
|
||||
def test_cache_health_report_serialization():
|
||||
"""Test that HealthReport can be serialized to dict"""
|
||||
from py.services.cache_health_monitor import HealthReport
|
||||
|
||||
report = HealthReport(
|
||||
status=CacheHealthStatus.DEGRADED,
|
||||
total_entries=100,
|
||||
valid_entries=98,
|
||||
invalid_entries=2,
|
||||
repaired_entries=1,
|
||||
invalid_paths=['/path1', '/path2'],
|
||||
message="Cache issues detected"
|
||||
)
|
||||
|
||||
result = report.to_dict()
|
||||
|
||||
assert result['status'] == 'degraded'
|
||||
assert result['total_entries'] == 100
|
||||
assert result['valid_entries'] == 98
|
||||
assert result['invalid_entries'] == 2
|
||||
assert result['repaired_entries'] == 1
|
||||
assert result['corruption_rate'] == '2.0%'
|
||||
assert len(result['invalid_paths']) == 2
|
||||
assert result['message'] == "Cache issues detected"
|
||||
Reference in New Issue
Block a user