1+ #!/usr/bin/env python3
2+ """
3+ Test to ensure privacy plugin resources are properly cached and not reloaded on each request.
4+ This test will fail if resources are being recreated on every call, preventing performance regressions.
5+ """
6+
7+ import time
8+ import sys
9+ import os
10+ from unittest .mock import Mock , patch , MagicMock
11+ import importlib
12+
13+ # Add parent directory to path
14+ sys .path .insert (0 , os .path .dirname (os .path .dirname (os .path .abspath (__file__ ))))
15+
16+ def test_privacy_plugin_resource_caching ():
17+ """
18+ Test that expensive resources (AnalyzerEngine, AnonymizerEngine) are created only once
19+ and reused across multiple plugin invocations.
20+ """
21+ print ("Testing privacy plugin resource caching..." )
22+
23+ # Need to reset the module state before testing
24+ if 'optillm.plugins.privacy_plugin' in sys .modules :
25+ del sys .modules ['optillm.plugins.privacy_plugin' ]
26+
27+ # Mock the expensive AnalyzerEngine and AnonymizerEngine at the module level before import
28+ with patch ('presidio_analyzer.AnalyzerEngine' ) as MockAnalyzerEngine , \
29+ patch ('presidio_anonymizer.AnonymizerEngine' ) as MockAnonymizerEngine , \
30+ patch ('spacy.util.is_package' , return_value = True ):
31+
32+ # Set up mock instances
33+ mock_analyzer_instance = MagicMock ()
34+ mock_analyzer_instance .analyze .return_value = []
35+ MockAnalyzerEngine .return_value = mock_analyzer_instance
36+
37+ mock_anonymizer_instance = MagicMock ()
38+ mock_anonymizer_instance .anonymize .return_value = MagicMock (text = "anonymized text" )
39+ mock_anonymizer_instance .add_anonymizer = MagicMock ()
40+ MockAnonymizerEngine .return_value = mock_anonymizer_instance
41+
42+ # Import the module with mocks in place
43+ import optillm .plugins .privacy_plugin as privacy_plugin
44+
45+ # Mock client for the run function
46+ mock_client = Mock ()
47+ mock_response = Mock ()
48+ mock_response .choices = [Mock (message = Mock (content = "response" ))]
49+ mock_response .usage .completion_tokens = 10
50+ mock_client .chat .completions .create .return_value = mock_response
51+
52+ # First invocation
53+ print ("First invocation..." )
54+ result1 , tokens1 = privacy_plugin .run ("system" , "query 1" , mock_client , "model" )
55+
56+ # Check that resources were created once
57+ assert MockAnalyzerEngine .call_count == 1 , f"AnalyzerEngine created { MockAnalyzerEngine .call_count } times, expected 1"
58+ assert MockAnonymizerEngine .call_count == 1 , f"AnonymizerEngine created { MockAnonymizerEngine .call_count } times, expected 1"
59+
60+ # Second invocation
61+ print ("Second invocation..." )
62+ result2 , tokens2 = privacy_plugin .run ("system" , "query 2" , mock_client , "model" )
63+
64+ # Check that resources were NOT created again
65+ assert MockAnalyzerEngine .call_count == 1 , f"AnalyzerEngine created { MockAnalyzerEngine .call_count } times after 2nd call, expected 1"
66+ assert MockAnonymizerEngine .call_count == 1 , f"AnonymizerEngine created { MockAnonymizerEngine .call_count } times after 2nd call, expected 1"
67+
68+ # Third invocation to be extra sure
69+ print ("Third invocation..." )
70+ result3 , tokens3 = privacy_plugin .run ("system" , "query 3" , mock_client , "model" )
71+
72+ # Still should be 1
73+ assert MockAnalyzerEngine .call_count == 1 , f"AnalyzerEngine created { MockAnalyzerEngine .call_count } times after 3rd call, expected 1"
74+ assert MockAnonymizerEngine .call_count == 1 , f"AnonymizerEngine created { MockAnonymizerEngine .call_count } times after 3rd call, expected 1"
75+
76+ print ("✅ Privacy plugin resource caching test PASSED - Resources are properly cached!" )
77+ return True
78+
79+ def test_privacy_plugin_performance ():
80+ """
81+ Test that multiple invocations of the privacy plugin don't have degraded performance.
82+ This catches the actual performance issue even without mocking.
83+ """
84+ print ("\n Testing privacy plugin performance (real execution)..." )
85+
86+ try :
87+ # Try to import the actual plugin
88+ import optillm .plugins .privacy_plugin as privacy_plugin
89+
90+ # Check if required dependencies are available
91+ try :
92+ import spacy
93+ from presidio_analyzer import AnalyzerEngine
94+ from presidio_anonymizer import AnonymizerEngine
95+ except ImportError as e :
96+ print (f"⚠️ Skipping performance test - dependencies not installed: { e } " )
97+ return True
98+
99+ # Mock client
100+ mock_client = Mock ()
101+ mock_response = Mock ()
102+ mock_response .choices = [Mock (message = Mock (content = "response" ))]
103+ mock_response .usage .completion_tokens = 10
104+ mock_client .chat .completions .create .return_value = mock_response
105+
106+ # Warm-up call (might include model download)
107+ print ("Warm-up call..." )
108+ start = time .time ()
109+ privacy_plugin .run ("system" , "warm up query" , mock_client , "model" )
110+ warmup_time = time .time () - start
111+ print (f"Warm-up time: { warmup_time :.2f} s" )
112+
113+ # First real measurement
114+ print ("First measurement call..." )
115+ start = time .time ()
116+ privacy_plugin .run ("system" , "test query 1" , mock_client , "model" )
117+ first_time = time .time () - start
118+ print (f"First call time: { first_time :.2f} s" )
119+
120+ # Second measurement - should be fast if caching works
121+ print ("Second measurement call..." )
122+ start = time .time ()
123+ privacy_plugin .run ("system" , "test query 2" , mock_client , "model" )
124+ second_time = time .time () - start
125+ print (f"Second call time: { second_time :.2f} s" )
126+
127+ # Third measurement
128+ print ("Third measurement call..." )
129+ start = time .time ()
130+ privacy_plugin .run ("system" , "test query 3" , mock_client , "model" )
131+ third_time = time .time () - start
132+ print (f"Third call time: { third_time :.2f} s" )
133+
134+ # Performance assertions
135+ # Second and third calls should be much faster than first (at least 10x faster)
136+ # Allow some tolerance for the first call as it might still be initializing
137+ max_acceptable_time = 2.0 # 2 seconds max for subsequent calls
138+
139+ if second_time > max_acceptable_time :
140+ raise AssertionError (f"Second call took { second_time :.2f} s, expected < { max_acceptable_time } s. Resources might not be cached!" )
141+
142+ if third_time > max_acceptable_time :
143+ raise AssertionError (f"Third call took { third_time :.2f} s, expected < { max_acceptable_time } s. Resources might not be cached!" )
144+
145+ print (f"✅ Privacy plugin performance test PASSED - Subsequent calls are fast ({ second_time :.2f} s, { third_time :.2f} s)!" )
146+ return True
147+
148+ except Exception as e :
149+ print (f"❌ Performance test failed: { e } " )
150+ raise
151+
152+ def test_singleton_instances_are_reused ():
153+ """
154+ Direct test that singleton instances are the same object across calls.
155+ """
156+ print ("\n Testing singleton instance reuse..." )
157+
158+ try :
159+ import optillm .plugins .privacy_plugin as privacy_plugin
160+ importlib .reload (privacy_plugin )
161+
162+ # Get first instances
163+ analyzer1 = privacy_plugin .get_analyzer_engine ()
164+ anonymizer1 = privacy_plugin .get_anonymizer_engine ()
165+
166+ # Get second instances
167+ analyzer2 = privacy_plugin .get_analyzer_engine ()
168+ anonymizer2 = privacy_plugin .get_anonymizer_engine ()
169+
170+ # They should be the exact same object
171+ assert analyzer1 is analyzer2 , "AnalyzerEngine instances are not the same object!"
172+ assert anonymizer1 is anonymizer2 , "AnonymizerEngine instances are not the same object!"
173+
174+ print ("✅ Singleton instance test PASSED - Same objects are reused!" )
175+ return True
176+
177+ except ImportError as e :
178+ print (f"⚠️ Skipping singleton test - dependencies not installed: { e } " )
179+ return True
180+ except Exception as e :
181+ print (f"❌ Singleton test failed: { e } " )
182+ raise
183+
184+ if __name__ == "__main__" :
185+ print ("=" * 60 )
186+ print ("Privacy Plugin Performance & Caching Tests" )
187+ print ("=" * 60 )
188+
189+ all_passed = True
190+
191+ try :
192+ test_privacy_plugin_resource_caching ()
193+ except Exception as e :
194+ all_passed = False
195+ print (f"❌ Resource caching test failed: { e } " )
196+
197+ try :
198+ test_singleton_instances_are_reused ()
199+ except Exception as e :
200+ all_passed = False
201+ print (f"❌ Singleton instance test failed: { e } " )
202+
203+ try :
204+ test_privacy_plugin_performance ()
205+ except Exception as e :
206+ all_passed = False
207+ print (f"❌ Performance test failed: { e } " )
208+
209+ print ("\n " + "=" * 60 )
210+ if all_passed :
211+ print ("✅ ALL TESTS PASSED!" )
212+ print ("Privacy plugin resources are properly cached." )
213+ sys .exit (0 )
214+ else :
215+ print ("❌ SOME TESTS FAILED!" )
216+ print ("Privacy plugin may have performance issues." )
217+ sys .exit (1 )
0 commit comments