Spaces:
Build error
Build error
| """Tests for LLMLingua-2 compressor integration. | |
| Comprehensive tests covering: | |
| - LLMLinguaConfig: Configuration validation and defaults | |
| - LLMLinguaCompressor: Core compression functionality | |
| - Transform interface: apply(), should_apply() methods | |
| - Content type detection: JSON, code, plain text | |
| - CCR integration: Reversible compression storage | |
| - Edge cases: Empty content, unavailable dependency, fallbacks | |
| """ | |
| import json | |
| from unittest.mock import MagicMock, patch | |
| import pytest | |
| from headroom.transforms.llmlingua_compressor import ( | |
| LLMLinguaCompressor, | |
| LLMLinguaConfig, | |
| LLMLinguaResult, | |
| compress_with_llmlingua, | |
| is_llmlingua_model_loaded, | |
| unload_llmlingua_model, | |
| ) | |
| # Try to import for availability check | |
| try: | |
| import llmlingua # noqa: F401 | |
| LLMLINGUA_INSTALLED = True | |
| except ImportError: | |
| LLMLINGUA_INSTALLED = False | |
| # ============================================================================= | |
| # Test Fixtures | |
| # ============================================================================= | |
| def default_config(): | |
| """Default LLMLinguaConfig for testing.""" | |
| return LLMLinguaConfig( | |
| min_tokens_for_compression=10, # Low threshold for tests | |
| enable_ccr=False, # Disable CCR for unit tests | |
| ) | |
| def compressor(default_config): | |
| """LLMLinguaCompressor instance with default config.""" | |
| return LLMLinguaCompressor(default_config) | |
| def mock_llmlingua(): | |
| """Mock the llmlingua module and PromptCompressor.""" | |
| mock_compressor = MagicMock() | |
| mock_compressor._model_name = "test-model" | |
| # Default compress_prompt return value | |
| mock_compressor.compress_prompt.return_value = { | |
| "compressed_prompt": "compressed content here", | |
| "origin_tokens": 100, | |
| "compressed_tokens": 30, | |
| } | |
| with patch( | |
| "headroom.transforms.llmlingua_compressor._check_llmlingua_available", | |
| return_value=True, | |
| ): | |
| with patch( | |
| "headroom.transforms.llmlingua_compressor._get_llmlingua_compressor", | |
| return_value=mock_compressor, | |
| ): | |
| yield mock_compressor | |
| def tokenizer(): | |
| """Get a tokenizer for Transform interface tests.""" | |
| from headroom.providers import OpenAIProvider | |
| from headroom.tokenizer import Tokenizer | |
| provider = OpenAIProvider() | |
| token_counter = provider.get_token_counter("gpt-4o") | |
| return Tokenizer(token_counter, "gpt-4o") | |
| # ============================================================================= | |
| # Test Data Generators | |
| # ============================================================================= | |
| def generate_long_text(n_words: int = 500) -> str: | |
| """Generate long text content for compression testing.""" | |
| words = ["the", "quick", "brown", "fox", "jumps", "over", "lazy", "dog"] | |
| return " ".join(words[i % len(words)] for i in range(n_words)) | |
| def generate_long_json(n_items: int = 50) -> str: | |
| """Generate long JSON content for compression testing.""" | |
| items = [ | |
| { | |
| "id": i, | |
| "name": f"Item {i}", | |
| "description": f"This is a detailed description for item number {i}", | |
| "value": i * 10, | |
| "active": i % 2 == 0, | |
| } | |
| for i in range(n_items) | |
| ] | |
| return json.dumps(items) | |
| def generate_long_code(n_functions: int = 20) -> str: | |
| """Generate Python code content for compression testing.""" | |
| lines = ['"""Module with many functions."""', "", "import os", "from typing import Any", ""] | |
| for i in range(n_functions): | |
| lines.extend( | |
| [ | |
| f"def function_{i}(arg: Any) -> str:", | |
| f' """Process argument {i}."""', | |
| " result = str(arg)", | |
| f' return f"Function {i}: {{result}}"', | |
| "", | |
| ] | |
| ) | |
| return "\n".join(lines) | |
| # ============================================================================= | |
| # TestLLMLinguaConfig | |
| # ============================================================================= | |
| class TestLLMLinguaConfig: | |
| """Tests for LLMLinguaConfig dataclass.""" | |
| def test_default_values(self): | |
| """Default config values are sensible.""" | |
| config = LLMLinguaConfig() | |
| assert config.model_name == "microsoft/llmlingua-2-xlm-roberta-large-meetingbank" | |
| assert config.device == "auto" | |
| assert config.target_compression_rate == 0.3 | |
| assert config.min_tokens_for_compression == 100 | |
| assert config.enable_ccr is True | |
| assert config.drop_consecutive is True | |
| def test_custom_values(self): | |
| """Custom config values are applied.""" | |
| config = LLMLinguaConfig( | |
| model_name="custom/model", | |
| device="cuda", | |
| target_compression_rate=0.5, | |
| min_tokens_for_compression=50, | |
| force_tokens=["important", "keep"], | |
| ) | |
| assert config.model_name == "custom/model" | |
| assert config.device == "cuda" | |
| assert config.target_compression_rate == 0.5 | |
| assert config.min_tokens_for_compression == 50 | |
| assert "important" in config.force_tokens | |
| def test_content_type_rates(self): | |
| """Different content types have appropriate compression rates.""" | |
| config = LLMLinguaConfig() | |
| # Code should be more conservative | |
| assert config.code_compression_rate > config.text_compression_rate | |
| # JSON should be between code and text | |
| assert config.json_compression_rate > config.text_compression_rate | |
| assert config.json_compression_rate < config.code_compression_rate | |
| # ============================================================================= | |
| # TestLLMLinguaResult | |
| # ============================================================================= | |
| class TestLLMLinguaResult: | |
| """Tests for LLMLinguaResult dataclass.""" | |
| def test_tokens_saved(self): | |
| """tokens_saved property calculates correctly.""" | |
| result = LLMLinguaResult( | |
| compressed="short", | |
| original="long content here", | |
| original_tokens=100, | |
| compressed_tokens=30, | |
| compression_ratio=0.3, | |
| ) | |
| assert result.tokens_saved == 70 | |
| def test_tokens_saved_no_negative(self): | |
| """tokens_saved never returns negative.""" | |
| result = LLMLinguaResult( | |
| compressed="expanded content", | |
| original="short", | |
| original_tokens=10, | |
| compressed_tokens=20, # Expanded (unusual case) | |
| compression_ratio=2.0, | |
| ) | |
| assert result.tokens_saved == 0 | |
| def test_savings_percentage(self): | |
| """savings_percentage property calculates correctly.""" | |
| result = LLMLinguaResult( | |
| compressed="short", | |
| original="long content", | |
| original_tokens=100, | |
| compressed_tokens=25, | |
| compression_ratio=0.25, | |
| ) | |
| assert result.savings_percentage == 75.0 | |
| def test_savings_percentage_zero_original(self): | |
| """savings_percentage handles zero original tokens.""" | |
| result = LLMLinguaResult( | |
| compressed="", | |
| original="", | |
| original_tokens=0, | |
| compressed_tokens=0, | |
| compression_ratio=1.0, | |
| ) | |
| assert result.savings_percentage == 0.0 | |
| # ============================================================================= | |
| # TestLLMLinguaCompressor | |
| # ============================================================================= | |
| class TestLLMLinguaCompressor: | |
| """Tests for LLMLinguaCompressor core functionality.""" | |
| def test_init_with_default_config(self): | |
| """Compressor initializes with default config.""" | |
| compressor = LLMLinguaCompressor() | |
| assert compressor.config is not None | |
| assert compressor.config.model_name is not None | |
| def test_init_with_custom_config(self, default_config): | |
| """Compressor initializes with custom config.""" | |
| compressor = LLMLinguaCompressor(default_config) | |
| assert compressor.config == default_config | |
| def test_compress_returns_result_when_unavailable(self, compressor): | |
| """Compress returns passthrough result when llmlingua unavailable.""" | |
| with patch( | |
| "headroom.transforms.llmlingua_compressor._check_llmlingua_available", | |
| return_value=False, | |
| ): | |
| content = generate_long_text(100) | |
| result = compressor.compress(content) | |
| # Should return unchanged content | |
| assert result.compressed == content | |
| assert result.compression_ratio == 1.0 | |
| def test_compress_skips_small_content(self, compressor): | |
| """Small content is not compressed.""" | |
| small_content = "short text" | |
| result = compressor.compress(small_content) | |
| assert result.compressed == small_content | |
| assert result.compression_ratio == 1.0 | |
| def test_compress_with_llmlingua(self, default_config, mock_llmlingua): | |
| """Compression uses llmlingua when available.""" | |
| compressor = LLMLinguaCompressor(default_config) | |
| content = generate_long_text(200) | |
| result = compressor.compress(content) | |
| # Should have called compress_prompt | |
| mock_llmlingua.compress_prompt.assert_called_once() | |
| assert result.compressed == "compressed content here" | |
| assert result.compression_ratio < 1.0 | |
| def test_compress_with_context(self, default_config, mock_llmlingua): | |
| """Context words are used as force tokens.""" | |
| compressor = LLMLinguaCompressor(default_config) | |
| content = generate_long_text(200) | |
| context = "important keywords here" | |
| compressor.compress(content, context=context) | |
| # Check force_tokens includes context words | |
| call_args = mock_llmlingua.compress_prompt.call_args | |
| force_tokens = call_args.kwargs.get("force_tokens", []) | |
| # Should include context words longer than 3 chars | |
| assert "important" in force_tokens or "keywords" in force_tokens | |
| def test_compress_handles_exception(self, default_config, mock_llmlingua): | |
| """Exceptions from llmlingua are handled gracefully.""" | |
| mock_llmlingua.compress_prompt.side_effect = RuntimeError("Model error") | |
| compressor = LLMLinguaCompressor(default_config) | |
| content = generate_long_text(200) | |
| result = compressor.compress(content) | |
| # Should return original content on error | |
| assert result.compressed == content | |
| assert result.compression_ratio == 1.0 | |
| # ============================================================================= | |
| # TestContentTypeDetection | |
| # ============================================================================= | |
| class TestContentTypeDetection: | |
| """Tests for content type auto-detection.""" | |
| def test_detect_json_content(self, default_config, mock_llmlingua): | |
| """JSON content is detected and uses JSON compression rate.""" | |
| compressor = LLMLinguaCompressor(default_config) | |
| rate = compressor._get_compression_rate(generate_long_json(50), None) | |
| assert rate == default_config.json_compression_rate | |
| def test_detect_code_content(self, default_config, mock_llmlingua): | |
| """Code content is detected and uses code compression rate.""" | |
| compressor = LLMLinguaCompressor(default_config) | |
| code = generate_long_code(20) | |
| rate = compressor._get_compression_rate(code, None) | |
| assert rate == default_config.code_compression_rate | |
| def test_detect_plain_text(self, default_config, mock_llmlingua): | |
| """Plain text uses text compression rate.""" | |
| compressor = LLMLinguaCompressor(default_config) | |
| text = generate_long_text(200) | |
| rate = compressor._get_compression_rate(text, None) | |
| assert rate == default_config.text_compression_rate | |
| def test_explicit_content_type(self, default_config, mock_llmlingua): | |
| """Explicit content_type overrides detection.""" | |
| compressor = LLMLinguaCompressor(default_config) | |
| # JSON-looking content but marked as text | |
| json_content = generate_long_json(50) | |
| rate = compressor._get_compression_rate(json_content, content_type="text") | |
| assert rate == default_config.text_compression_rate | |
| def test_looks_like_json_detection(self, default_config): | |
| """JSON detection works for arrays and objects.""" | |
| compressor = LLMLinguaCompressor(default_config) | |
| assert compressor._looks_like_json('[{"key": "value"}]') | |
| assert compressor._looks_like_json('{"key": "value"}') | |
| assert not compressor._looks_like_json("plain text") | |
| assert not compressor._looks_like_json("def function():") | |
| def test_looks_like_code_detection(self, default_config): | |
| """Code detection works for common patterns.""" | |
| compressor = LLMLinguaCompressor(default_config) | |
| assert compressor._looks_like_code("def function():") | |
| assert compressor._looks_like_code("class MyClass:") | |
| assert compressor._looks_like_code("import os") | |
| assert compressor._looks_like_code("function test() {") | |
| assert compressor._looks_like_code("const x = 5") | |
| assert not compressor._looks_like_code("plain text content") | |
| # ============================================================================= | |
| # TestTransformInterface | |
| # ============================================================================= | |
| class TestTransformInterface: | |
| """Tests for Transform interface (apply, should_apply).""" | |
| def test_should_apply_returns_false_when_unavailable(self, compressor, tokenizer): | |
| """should_apply returns False when llmlingua unavailable.""" | |
| messages = [{"role": "user", "content": generate_long_text(200)}] | |
| with patch( | |
| "headroom.transforms.llmlingua_compressor._check_llmlingua_available", | |
| return_value=False, | |
| ): | |
| assert not compressor.should_apply(messages, tokenizer) | |
| def test_should_apply_returns_false_for_small_content(self, default_config, tokenizer): | |
| """should_apply returns False for small content.""" | |
| config = LLMLinguaConfig(min_tokens_for_compression=1000) | |
| compressor = LLMLinguaCompressor(config) | |
| messages = [{"role": "user", "content": "small"}] | |
| with patch( | |
| "headroom.transforms.llmlingua_compressor._check_llmlingua_available", | |
| return_value=True, | |
| ): | |
| assert not compressor.should_apply(messages, tokenizer) | |
| def test_should_apply_returns_true_for_large_content(self, default_config, tokenizer): | |
| """should_apply returns True for large content.""" | |
| compressor = LLMLinguaCompressor(default_config) | |
| messages = [{"role": "user", "content": generate_long_text(500)}] | |
| with patch( | |
| "headroom.transforms.llmlingua_compressor._check_llmlingua_available", | |
| return_value=True, | |
| ): | |
| assert compressor.should_apply(messages, tokenizer) | |
| def test_apply_compresses_tool_messages(self, default_config, tokenizer, mock_llmlingua): | |
| """apply() compresses tool message content.""" | |
| compressor = LLMLinguaCompressor(default_config) | |
| tool_content = generate_long_json(100) | |
| messages = [ | |
| {"role": "user", "content": "Get data"}, | |
| {"role": "tool", "tool_call_id": "call_1", "content": tool_content}, | |
| ] | |
| result = compressor.apply(messages, tokenizer) | |
| # Tool content should be compressed | |
| assert result.messages[1]["content"] != tool_content | |
| assert "compressed content here" in result.messages[1]["content"] | |
| assert len(result.transforms_applied) > 0 | |
| def test_apply_compresses_long_assistant_messages( | |
| self, default_config, tokenizer, mock_llmlingua | |
| ): | |
| """apply() compresses long assistant messages.""" | |
| compressor = LLMLinguaCompressor(default_config) | |
| long_content = generate_long_text(1000) | |
| messages = [ | |
| {"role": "user", "content": "Tell me a story"}, | |
| {"role": "assistant", "content": long_content}, | |
| ] | |
| result = compressor.apply(messages, tokenizer) | |
| # Assistant content should be compressed (>500 chars) | |
| assert result.messages[1]["content"] != long_content | |
| def test_apply_passes_through_short_messages(self, default_config, tokenizer, mock_llmlingua): | |
| """apply() passes through short messages unchanged.""" | |
| compressor = LLMLinguaCompressor(default_config) | |
| messages = [ | |
| {"role": "user", "content": "Hello"}, | |
| {"role": "assistant", "content": "Hi there!"}, | |
| ] | |
| result = compressor.apply(messages, tokenizer) | |
| # Short messages unchanged | |
| assert result.messages[0]["content"] == "Hello" | |
| assert result.messages[1]["content"] == "Hi there!" | |
| def test_apply_tracks_transform_metadata(self, default_config, tokenizer, mock_llmlingua): | |
| """apply() returns proper TransformResult metadata.""" | |
| compressor = LLMLinguaCompressor(default_config) | |
| messages = [ | |
| {"role": "tool", "tool_call_id": "call_1", "content": generate_long_json(100)}, | |
| ] | |
| result = compressor.apply(messages, tokenizer) | |
| assert result.tokens_before > 0 | |
| assert result.tokens_after > 0 | |
| assert len(result.transforms_applied) > 0 | |
| assert "llmlingua" in result.transforms_applied[0] | |
| def test_apply_adds_warning_when_unavailable(self, default_config, tokenizer): | |
| """apply() adds warning when llmlingua unavailable.""" | |
| compressor = LLMLinguaCompressor(default_config) | |
| messages = [{"role": "user", "content": "test"}] | |
| with patch( | |
| "headroom.transforms.llmlingua_compressor._check_llmlingua_available", | |
| return_value=False, | |
| ): | |
| result = compressor.apply(messages, tokenizer) | |
| assert len(result.warnings) > 0 | |
| assert "llmlingua" in result.warnings[0].lower() | |
| # ============================================================================= | |
| # TestDeviceResolution | |
| # ============================================================================= | |
| class TestDeviceResolution: | |
| """Tests for device resolution logic.""" | |
| def test_resolve_explicit_device(self, default_config): | |
| """Explicit device is returned unchanged.""" | |
| config = LLMLinguaConfig(device="cuda") | |
| compressor = LLMLinguaCompressor(config) | |
| assert compressor._resolve_device() == "cuda" | |
| def test_resolve_auto_to_cpu_no_torch(self, default_config): | |
| """Auto resolves to CPU when torch unavailable.""" | |
| config = LLMLinguaConfig(device="auto") | |
| compressor = LLMLinguaCompressor(config) | |
| with patch.dict("sys.modules", {"torch": None}): | |
| with patch( | |
| "headroom.transforms.llmlingua_compressor.LLMLinguaCompressor._resolve_device" | |
| ) as mock_resolve: | |
| mock_resolve.return_value = "cpu" | |
| assert compressor._resolve_device() == "cpu" | |
| # ============================================================================= | |
| # TestCCRIntegration | |
| # ============================================================================= | |
| class TestCCRIntegration: | |
| """Tests for CCR (Compress-Cache-Retrieve) integration.""" | |
| def test_ccr_stores_original(self, mock_llmlingua): | |
| """Compressed content is stored in CCR when enabled.""" | |
| config = LLMLinguaConfig( | |
| enable_ccr=True, | |
| min_tokens_for_compression=10, | |
| ) | |
| compressor = LLMLinguaCompressor(config) | |
| content = generate_long_text(200) | |
| with patch( | |
| "headroom.transforms.llmlingua_compressor.LLMLinguaCompressor._store_in_ccr" | |
| ) as mock_store: | |
| mock_store.return_value = "hash123" | |
| result = compressor.compress(content) | |
| mock_store.assert_called_once() | |
| assert result.cache_key == "hash123" | |
| def test_ccr_skipped_when_disabled(self, mock_llmlingua): | |
| """CCR is not used when disabled in config.""" | |
| config = LLMLinguaConfig( | |
| enable_ccr=False, | |
| min_tokens_for_compression=10, | |
| ) | |
| compressor = LLMLinguaCompressor(config) | |
| content = generate_long_text(200) | |
| with patch( | |
| "headroom.transforms.llmlingua_compressor.LLMLinguaCompressor._store_in_ccr" | |
| ) as mock_store: | |
| result = compressor.compress(content) | |
| mock_store.assert_not_called() | |
| assert result.cache_key is None | |
| def test_ccr_handles_storage_error(self, mock_llmlingua): | |
| """CCR storage errors are handled gracefully.""" | |
| config = LLMLinguaConfig( | |
| enable_ccr=True, | |
| min_tokens_for_compression=10, | |
| ) | |
| compressor = LLMLinguaCompressor(config) | |
| content = generate_long_text(200) | |
| with patch( | |
| "headroom.transforms.llmlingua_compressor.LLMLinguaCompressor._store_in_ccr" | |
| ) as mock_store: | |
| # Return None to simulate storage failure (internal error handling) | |
| mock_store.return_value = None | |
| # Should not raise | |
| result = compressor.compress(content) | |
| # Storage failed, so cache_key should be None | |
| assert result.cache_key is None | |
| # ============================================================================= | |
| # TestConvenienceFunction | |
| # ============================================================================= | |
| class TestConvenienceFunction: | |
| """Tests for compress_with_llmlingua convenience function.""" | |
| def test_compress_with_llmlingua_basic(self, mock_llmlingua): | |
| """compress_with_llmlingua works with default settings.""" | |
| content = generate_long_text(200) | |
| # Disable CCR for this test to avoid hash suffix | |
| with patch( | |
| "headroom.transforms.llmlingua_compressor.LLMLinguaCompressor._store_in_ccr" | |
| ) as mock_store: | |
| mock_store.return_value = None | |
| result = compress_with_llmlingua(content) | |
| # Should contain the compressed content | |
| assert "compressed content here" in result | |
| def test_compress_with_llmlingua_custom_rate(self, mock_llmlingua): | |
| """compress_with_llmlingua accepts custom compression rate.""" | |
| content = generate_long_text(200) | |
| compress_with_llmlingua(content, compression_rate=0.5) | |
| # Verify compress_prompt was called | |
| mock_llmlingua.compress_prompt.assert_called() | |
| def test_compress_with_llmlingua_with_context(self, mock_llmlingua): | |
| """compress_with_llmlingua passes context.""" | |
| content = generate_long_text(200) | |
| context = "important keywords" | |
| compress_with_llmlingua(content, context=context) | |
| call_args = mock_llmlingua.compress_prompt.call_args | |
| force_tokens = call_args.kwargs.get("force_tokens", []) | |
| # Context words should be in force_tokens | |
| assert any("important" in str(t) for t in force_tokens) or len(force_tokens) > 0 | |
| # ============================================================================= | |
| # TestEdgeCases | |
| # ============================================================================= | |
| class TestEdgeCases: | |
| """Edge case tests for LLMLingua compressor.""" | |
| def test_empty_content(self, compressor): | |
| """Empty content is handled gracefully.""" | |
| result = compressor.compress("") | |
| assert result.compressed == "" | |
| assert result.compression_ratio == 1.0 | |
| def test_whitespace_only_content(self, compressor): | |
| """Whitespace-only content is handled gracefully.""" | |
| result = compressor.compress(" \n\t\n ") | |
| assert result.compression_ratio == 1.0 | |
| def test_unicode_content(self, default_config, mock_llmlingua): | |
| """Unicode content is handled correctly.""" | |
| mock_llmlingua.compress_prompt.return_value = { | |
| "compressed_prompt": "compressed \u4e2d\u6587 content", | |
| "origin_tokens": 100, | |
| "compressed_tokens": 30, | |
| } | |
| compressor = LLMLinguaCompressor(default_config) | |
| content = "\u4e2d\u6587 \u65e5\u672c\u8a9e " * 100 # Chinese/Japanese text | |
| result = compressor.compress(content) | |
| assert "\u4e2d\u6587" in result.compressed | |
| def test_very_long_content(self, default_config, mock_llmlingua): | |
| """Very long content is compressed.""" | |
| compressor = LLMLinguaCompressor(default_config) | |
| content = generate_long_text(10000) | |
| compressor.compress(content) | |
| mock_llmlingua.compress_prompt.assert_called_once() | |
| def test_mixed_content_types(self, default_config, mock_llmlingua): | |
| """Mixed content (JSON with text) is handled.""" | |
| compressor = LLMLinguaCompressor(default_config) | |
| # JSON-like but with extra text | |
| content = 'Some preamble text\n{"key": "value"}\nMore text after' | |
| # Should not crash | |
| result = compressor.compress(content) | |
| assert result is not None | |
| def test_malformed_json_content(self, default_config, mock_llmlingua): | |
| """Malformed JSON is treated as text.""" | |
| compressor = LLMLinguaCompressor(default_config) | |
| content = "{malformed: json, missing quotes" * 50 | |
| rate = compressor._get_compression_rate(content, None) | |
| # Should not detect as JSON | |
| assert rate == default_config.text_compression_rate | |
| def test_force_tokens_list_handling(self, default_config, mock_llmlingua): | |
| """Force tokens list is properly passed.""" | |
| config = LLMLinguaConfig( | |
| force_tokens=["keep", "these", "tokens"], | |
| min_tokens_for_compression=10, | |
| ) | |
| compressor = LLMLinguaCompressor(config) | |
| content = generate_long_text(200) | |
| compressor.compress(content) | |
| call_args = mock_llmlingua.compress_prompt.call_args | |
| force_tokens = call_args.kwargs.get("force_tokens", []) | |
| assert "keep" in force_tokens | |
| assert "these" in force_tokens | |
| assert "tokens" in force_tokens | |
| # ============================================================================= | |
| # Integration Tests (only run if llmlingua is installed) | |
| # ============================================================================= | |
| class TestLLMLinguaIntegration: | |
| """Integration tests that require actual llmlingua installation. | |
| These tests verify the actual compression behavior and should be run | |
| in environments where llmlingua is installed. | |
| """ | |
| def test_actual_compression(self): | |
| """Test actual compression with real llmlingua.""" | |
| config = LLMLinguaConfig( | |
| target_compression_rate=0.3, | |
| min_tokens_for_compression=50, | |
| enable_ccr=False, | |
| ) | |
| compressor = LLMLinguaCompressor(config) | |
| content = generate_long_text(500) | |
| result = compressor.compress(content) | |
| # Should achieve actual compression | |
| assert result.compression_ratio < 1.0 | |
| assert result.tokens_saved > 0 | |
| assert len(result.compressed) < len(content) | |
| def test_actual_json_compression(self): | |
| """Test JSON content compression with real llmlingua.""" | |
| config = LLMLinguaConfig( | |
| target_compression_rate=0.35, | |
| min_tokens_for_compression=50, | |
| enable_ccr=False, | |
| ) | |
| compressor = LLMLinguaCompressor(config) | |
| content = generate_long_json(50) | |
| result = compressor.compress(content, content_type="json") | |
| assert result.compression_ratio < 1.0 | |
| def test_actual_code_compression(self): | |
| """Test code content compression with real llmlingua.""" | |
| config = LLMLinguaConfig( | |
| target_compression_rate=0.4, | |
| min_tokens_for_compression=50, | |
| enable_ccr=False, | |
| ) | |
| compressor = LLMLinguaCompressor(config) | |
| content = generate_long_code(30) | |
| result = compressor.compress(content, content_type="code") | |
| assert result.compression_ratio < 1.0 | |
| # ============================================================================= | |
| # TestMemoryManagement | |
| # ============================================================================= | |
| class TestMemoryManagement: | |
| """Tests for memory management functions (unload_llmlingua_model, is_llmlingua_model_loaded).""" | |
| def test_is_model_loaded_returns_false_initially(self): | |
| """is_llmlingua_model_loaded returns False when no model loaded.""" | |
| # Ensure model is unloaded | |
| with patch( | |
| "headroom.transforms.llmlingua_compressor._llmlingua_instance", | |
| None, | |
| ): | |
| assert is_llmlingua_model_loaded() is False | |
| def test_is_model_loaded_returns_true_when_loaded(self): | |
| """is_llmlingua_model_loaded returns True when model is loaded.""" | |
| mock_instance = MagicMock() | |
| with patch( | |
| "headroom.transforms.llmlingua_compressor._llmlingua_instance", | |
| mock_instance, | |
| ): | |
| assert is_llmlingua_model_loaded() is True | |
| def test_unload_returns_false_when_no_model(self): | |
| """unload_llmlingua_model returns False when no model loaded.""" | |
| import headroom.transforms.llmlingua_compressor as module | |
| # Save original | |
| original = module._llmlingua_instance | |
| try: | |
| module._llmlingua_instance = None | |
| result = unload_llmlingua_model() | |
| assert result is False | |
| finally: | |
| module._llmlingua_instance = original | |
| def test_unload_clears_instance(self): | |
| """unload_llmlingua_model clears the global instance.""" | |
| import headroom.transforms.llmlingua_compressor as module | |
| # Save original | |
| original = module._llmlingua_instance | |
| try: | |
| # Set a mock instance | |
| mock_instance = MagicMock() | |
| mock_instance._model_name = "test-model" | |
| module._llmlingua_instance = mock_instance | |
| # Unload | |
| result = unload_llmlingua_model() | |
| assert result is True | |
| assert module._llmlingua_instance is None | |
| finally: | |
| module._llmlingua_instance = original | |
| def test_unload_clears_cuda_cache(self): | |
| """unload_llmlingua_model attempts to clear CUDA cache.""" | |
| import headroom.transforms.llmlingua_compressor as module | |
| original = module._llmlingua_instance | |
| try: | |
| mock_instance = MagicMock() | |
| mock_instance._model_name = "test-model" | |
| module._llmlingua_instance = mock_instance | |
| mock_torch = MagicMock() | |
| mock_torch.cuda.is_available.return_value = True | |
| with patch.dict("sys.modules", {"torch": mock_torch}): | |
| with patch( | |
| "headroom.transforms.llmlingua_compressor.torch", | |
| mock_torch, | |
| create=True, | |
| ): | |
| result = unload_llmlingua_model() | |
| assert result is True | |
| finally: | |
| module._llmlingua_instance = original | |
| # ============================================================================= | |
| # TestThreadSafety | |
| # ============================================================================= | |
| class TestThreadSafety: | |
| """Tests for thread safety of model loading.""" | |
| def test_lock_exists(self): | |
| """Verify thread lock is available.""" | |
| import headroom.transforms.llmlingua_compressor as module | |
| assert hasattr(module, "_llmlingua_lock") | |
| import threading | |
| assert isinstance(module._llmlingua_lock, type(threading.Lock())) | |
| # ============================================================================= | |
| # TestErrorMessages | |
| # ============================================================================= | |
| class TestErrorMessages: | |
| """Tests for improved error messages.""" | |
| def test_import_error_message_includes_install_hint(self): | |
| """ImportError includes installation instructions.""" | |
| with patch( | |
| "headroom.transforms.llmlingua_compressor._check_llmlingua_available", | |
| return_value=False, | |
| ): | |
| from headroom.transforms.llmlingua_compressor import _get_llmlingua_compressor | |
| with pytest.raises(ImportError) as exc_info: | |
| _get_llmlingua_compressor("test-model", "cpu") | |
| error_msg = str(exc_info.value) | |
| assert "pip install headroom-ai[llmlingua]" in error_msg | |
| assert "2GB" in error_msg or "disk space" in error_msg.lower() | |
| def test_oom_error_provides_helpful_suggestions(self): | |
| """Out of memory error provides helpful suggestions.""" | |
| import headroom.transforms.llmlingua_compressor as module | |
| # Save original state | |
| original_instance = module._llmlingua_instance | |
| original_available = module._llmlingua_available | |
| try: | |
| module._llmlingua_instance = None | |
| module._llmlingua_available = True | |
| # Create a mock that raises OOM when called | |
| mock_prompt_compressor_class = MagicMock() | |
| mock_prompt_compressor_class.side_effect = RuntimeError("CUDA out of memory") | |
| with patch.dict("sys.modules", {"llmlingua": MagicMock()}): | |
| with patch( | |
| "llmlingua.PromptCompressor", | |
| mock_prompt_compressor_class, | |
| ): | |
| from headroom.transforms.llmlingua_compressor import ( | |
| _get_llmlingua_compressor, | |
| ) | |
| with pytest.raises(RuntimeError) as exc_info: | |
| _get_llmlingua_compressor("test-model", "cuda") | |
| error_msg = str(exc_info.value) | |
| # Should include helpful suggestions | |
| assert "cpu" in error_msg.lower() or "memory" in error_msg.lower() | |
| finally: | |
| module._llmlingua_instance = original_instance | |
| module._llmlingua_available = original_available | |