22import sys
33import os
44import importlib
5+ import signal
56from unittest .mock import MagicMock , patch , mock_open
67
78# -------------------------------------------------------------------------
8- # 1. GLOBAL MOCKS (The "Matrix")
9- # We mock the entire physical world to ensure tests run on GitHub.
9+ # 1. GLOBAL MOCKS
1010# -------------------------------------------------------------------------
11+ # Mock GUI elements
1112sys .modules ['tkinter' ] = MagicMock ()
1213sys .modules ['tkinter.ttk' ] = MagicMock ()
1314sys .modules ['tkinter.messagebox' ] = MagicMock ()
1415sys .modules ['tkinter.filedialog' ] = MagicMock ()
1516
16- # Matplotlib Mocks
17+ # Mock Multiprocessing to prevent Queue.get() hangs
18+ mock_mp = MagicMock ()
19+ sys .modules ['multiprocessing' ] = mock_mp
20+ sys .modules ['multiprocessing.queues' ] = MagicMock ()
21+
22+ # Mock Matplotlib
1723mock_plt = MagicMock ()
1824mock_fig = MagicMock ()
1925mock_ax = MagicMock ()
20- mock_plt .subplots .return_value = (mock_fig , mock_ax )
21- mock_plt .subplots .side_effect = None
22-
26+ mock_plt .subplots .return_value = (mock_fig , mock_ax )
2327sys .modules ['matplotlib' ] = MagicMock ()
2428sys .modules ['matplotlib.pyplot' ] = mock_plt
2529sys .modules ['matplotlib.figure' ] = MagicMock ()
2933class TestDeepSimulation (unittest .TestCase ):
3034
3135 def setUp (self ):
32- # Add project root to path
3336 self .root_dir = os .path .abspath (os .path .join (os .path .dirname (__file__ ), '..' ))
3437 if self .root_dir not in sys .path :
3538 sys .path .insert (0 , self .root_dir )
39+ print (f"\n [TEST START] { self ._testMethodName } " , flush = True )
3640
37- def get_circuit_breaker (self , limit = 10 ):
38- """Returns a side_effect for time.sleep that raises exception after limit."""
39- return [None ] * limit + [Exception ("Force Test Exit" )]
41+ def tearDown (self ):
42+ print (f"[TEST END] { self ._testMethodName } \n " , flush = True )
43+
44+ # -------------------------------------------------------------------------
45+ # HELPER: The "Watchdog" Timer
46+ # -------------------------------------------------------------------------
47+ def _timeout_handler (self , signum , frame ):
48+ raise TimeoutError (f"Test { self ._testMethodName } took longer than 30s! Infinite Loop suspected." )
4049
4150 def run_module_safely (self , module_name ):
42- """Helper: Import module, run main() if exists, handle 'Force Exit'."""
51+ """Imports and runs a module with a strict 30-second timeout."""
52+ # Set an alarm for 30 seconds (Works on Linux/GitHub Actions)
53+ signal .signal (signal .SIGALRM , self ._timeout_handler )
54+ signal .alarm (30 )
55+
4356 if module_name in sys .modules :
4457 del sys .modules [module_name ]
4558
4659 try :
60+ print (f" -> Importing { module_name } ..." , flush = True )
4761 mod = importlib .import_module (module_name )
4862 if hasattr (mod , 'main' ):
63+ print (f" -> Running { module_name } .main()..." , flush = True )
4964 mod .main ()
65+ else :
66+ print (f" -> Module loaded (no main function)." , flush = True )
5067 except Exception as e :
51- # Ignore expected exit signals from our circuit breakers
52- if "Force Test Exit" in str (e ) or isinstance (e , KeyboardInterrupt ) or isinstance (e , SystemExit ):
53- pass
68+ if "Force Test Exit" in str (e ) or isinstance (e , SystemExit ):
69+ print (" -> [SUCCESS] Script exited cleanly via Circuit Breaker." , flush = True )
70+ elif isinstance (e , TimeoutError ):
71+ print (f" -> [FAIL] CRITICAL TIMEOUT: { e } " , flush = True )
72+ raise e # Re-raise to fail the test
5473 else :
55- print (f" [Info] Script '{ module_name } ' stopped with: { e } " )
74+ print (f" -> [INFO] Script stopped with: { e } " , flush = True )
75+ finally :
76+ signal .alarm (0 ) # Disable the alarm
77+
78+ def get_circuit_breaker (self , limit = 10 ):
79+ """A mock sleep that counts down and raises an error to break infinite loops."""
80+ def side_effect (* args , ** kwargs ):
81+ side_effect .counter += 1
82+ # Print a heartbeat so we know the loop is actually running
83+ if side_effect .counter % 2 == 0 :
84+ print (f" [Clock] Tick { side_effect .counter } /{ limit } ..." , flush = True )
85+
86+ if side_effect .counter >= limit :
87+ print (" [Clock] Limit reached! Forcing exit." , flush = True )
88+ raise Exception ("Force Test Exit" )
89+
90+ side_effect .counter = 0
91+ return side_effect
5692
5793 # =========================================================================
58- # SECTION 1: MAIN MEASUREMENT MODULES
94+ # TESTS
5995 # =========================================================================
6096
6197 def test_01_k2400_iv_backend (self ):
62- print ("\n [SIMULATION] 1. Keithley 2400 I-V Sweep..." )
63- with patch ('pymeasure.instruments.keithley.Keithley2400' ) as MockInst :
98+ # GLOBAL PATCH for sleep is critical here
99+ with patch ('pymeasure.instruments.keithley.Keithley2400' ) as MockInst , \
100+ patch ('time.sleep' , side_effect = self .get_circuit_breaker (5 )):
101+
64102 spy = MockInst .return_value
65- # FIXED: Patch time.sleep globally instead of specific path to ensure capture
66- breaker = self .get_circuit_breaker (5 )
67- with patch ('builtins.input' , side_effect = ['100' , '10' , 'test' ]), \
68- patch ('pandas.DataFrame.to_csv' ), \
69- patch ('time.sleep' , side_effect = breaker ):
103+ with patch ('builtins.input' , side_effect = ['100' , '10' , 'test_file' ]), \
104+ patch ('pandas.DataFrame.to_csv' ):
70105 self .run_module_safely ("Keithley_2400.Backends.IV_K2400_Loop_Backend_v10" )
71106 spy .enable_source .assert_called ()
72107
73108 def test_02_lakeshore_backend (self ):
74- print ("\n [SIMULATION] 2. Lakeshore 350 Control..." )
75- with patch ('pyvisa.ResourceManager' ) as MockRM :
109+ with patch ('pyvisa.ResourceManager' ) as MockRM , \
110+ patch ('time.sleep' , side_effect = self .get_circuit_breaker (15 )):
111+
76112 spy = MockRM .return_value .open_resource .return_value
77- spy .query .side_effect = ["LSCI,MODEL350,0,0" ] + ["10.0" , "10.1" , "10.2" , " 300.0" ] * 50
78- breaker = self . get_circuit_breaker ( 15 )
113+ spy .query .side_effect = ["LSCI,MODEL350,0,0" ] + ["10.0" , "300.0" ] * 20
114+
79115 with patch ('builtins.input' , side_effect = ['10' , '300' , '10' , '350' ]), \
80116 patch ('builtins.open' , mock_open ()), \
81- patch ('time.sleep' , side_effect = breaker ), \
82117 patch ('tkinter.filedialog.asksaveasfilename' , return_value = "test.csv" ), \
83118 patch ('matplotlib.pyplot.show' ):
84119 self .run_module_safely ("Lakeshore_350_340.Backends.T_Control_L350_Simple_Backend_v10" )
85120
86121 def test_03_k6517b_pyro_backend (self ):
87- print ( " \n [SIMULATION] 3. Keithley 6517B Pyroelectric..." )
88- with patch ('pymeasure.instruments.keithley.Keithley6517B' ) as MockInst :
122+ with patch ( 'pymeasure.instruments.keithley.Keithley6517B' ) as MockInst , \
123+ patch ('time.sleep' , side_effect = self . get_circuit_breaker ( 5 )) :
89124 spy = MockInst .return_value
90125 spy .current = 1.23e-9
91- breaker = self .get_circuit_breaker (5 )
92- with patch ('pandas.DataFrame.to_csv' ), patch ('time.sleep' , side_effect = breaker ):
126+ with patch ('pandas.DataFrame.to_csv' ):
93127 self .run_module_safely ("Keithley_6517B.Pyroelectricity.Backends.Current_K6517B_Simple_Backend_v10" )
94128
95129 def test_04_lcr_keysight_backend (self ):
96- print ("\n [SIMULATION] 4. Keysight E4980A LCR..." )
97130 with patch ('pymeasure.instruments.agilent.AgilentE4980' ), \
98- patch ('pyvisa.ResourceManager' ) as MockRM :
131+ patch ('pyvisa.ResourceManager' ) as MockRM , \
132+ patch ('time.sleep' , side_effect = self .get_circuit_breaker (5 )):
99133 visa_spy = MockRM .return_value .open_resource .return_value
100134 visa_spy .query .return_value = "0.5"
101- breaker = self .get_circuit_breaker (5 )
102- with patch ('pandas.DataFrame.to_csv' ), patch ('time.sleep' , side_effect = breaker ):
135+ with patch ('pandas.DataFrame.to_csv' ):
103136 self .run_module_safely ("LCR_Keysight_E4980A.Backends.CV_KE4980A_Simple_Backend_v10" )
104137
105- # =========================================================================
106- # SECTION 2: COMPLEX & COMBINED MODULES
107- # =========================================================================
108-
109138 def test_05_delta_simple (self ):
110- print ( " \n [SIMULATION] 5. Delta Mode (Simple)..." )
111- with patch ('pyvisa.ResourceManager' ) as MockRM :
139+ with patch ( 'pyvisa.ResourceManager' ) as MockRM , \
140+ patch ('time.sleep' , side_effect = self . get_circuit_breaker ( 10 )) :
112141 k6221 = MockRM .return_value .open_resource .return_value
113- breaker = self .get_circuit_breaker (10 )
114142 inputs = ['0' , '1e-5' , '1e-6' , 'test_file' , 'y' , 'y' ]
115143 with patch ('builtins.input' , side_effect = inputs ), \
116- patch ('pandas.DataFrame.to_csv' ), \
117- patch ('time.sleep' , side_effect = breaker ):
144+ patch ('pandas.DataFrame.to_csv' ):
118145 self .run_module_safely ("Delta_mode_Keithley_6221_2182.Backends.Delta_K6221_K2182_Simple_v7" )
119146
120147 def test_06_delta_sensing (self ):
121- print ( " \n [SIMULATION] 6. Delta Mode (T-Sensing)..." )
122- with patch ('pyvisa.ResourceManager' ) as MockRM :
148+ with patch ( 'pyvisa.ResourceManager' ) as MockRM , \
149+ patch ('time.sleep' , side_effect = self . get_circuit_breaker ( 10 )) :
123150 inst = MockRM .return_value .open_resource .return_value
124- inst .query .return_value = "+1.23E-5"
125- breaker = self .get_circuit_breaker (10 )
151+ inst .query .return_value = "+1.23E-5"
126152 inputs = ['10' , '300' , '10' , 'test_file' , 'y' ]
127153 with patch ('builtins.input' , side_effect = inputs ), \
128- patch ('pandas.DataFrame.to_csv' ), \
129- patch ('time.sleep' , side_effect = breaker ):
154+ patch ('pandas.DataFrame.to_csv' ):
130155 try :
131156 self .run_module_safely ("Delta_mode_Keithley_6221_2182.Backends.Delta_K6221_K2182_L350_T_Sensing_Backend_v1" )
132157 except ModuleNotFoundError :
133- print (" [Skip] Delta Sensing script not found" )
158+ print (" [SKIP] Module not found, skipping. " )
134159
135160 def test_07_lockin_backend (self ):
136- print ( " \n [SIMULATION] 7. Lock-in Amplifier SR830..." )
137- with patch ('pyvisa.ResourceManager' ) as MockRM :
161+ with patch ( 'pyvisa.ResourceManager' ) as MockRM , \
162+ patch ('time.sleep' , side_effect = self . get_circuit_breaker ( 5 )) :
138163 spy = MockRM .return_value .open_resource .return_value
139164 spy .query .return_value = "1.23,4.56"
140- breaker = self .get_circuit_breaker (5 )
141- with patch ('time.sleep' , side_effect = breaker ):
142- self .run_module_safely ("Lock_in_amplifier.BasicTest_S830_Backend_v1" )
165+ self .run_module_safely ("Lock_in_amplifier.BasicTest_S830_Backend_v1" )
143166
144167 def test_08_combined_2400_2182 (self ):
145- print ("\n [SIMULATION] 8. Combined K2400 + K2182..." )
146- with patch ('pyvisa.ResourceManager' ) as MockRM :
168+ # THIS WAS THE TEST CAUSING THE HANG
169+ # We suspect input mismatch or resource opening hang.
170+ with patch ('pyvisa.ResourceManager' ) as MockRM , \
171+ patch ('time.sleep' , side_effect = self .get_circuit_breaker (10 )):
172+
147173 rm = MockRM .return_value
148- rm .open_resource .side_effect = [MagicMock (), MagicMock (), MagicMock ()]
149- breaker = self .get_circuit_breaker (10 )
150- inputs = ['10' , '1' , 'test_file' , 'y' ]
174+ # Ensure side_effect doesn't run out if script asks for many resources
175+ rm .open_resource .return_value = MagicMock ()
176+
177+ # Add extra inputs just in case the script asks for more than expected
178+ inputs = ['10' , '1' , 'test_file' , 'y' , 'y' , 'y' , 'y' ]
179+
151180 with patch ('builtins.input' , side_effect = inputs ), \
152- patch ('pandas.DataFrame.to_csv' ), \
153- patch ('time.sleep' , side_effect = breaker ):
181+ patch ('pandas.DataFrame.to_csv' ):
154182 self .run_module_safely ("Keithley_2400_Keithley_2182.Backends.IV_K2400_K2182_Backend_v1" )
155183
156184 def test_09_poling (self ):
157- print ( " \n [SIMULATION] 9. Poling K6517B..." )
158- with patch ('pyvisa.ResourceManager' ) as MockRM :
185+ with patch ( 'pyvisa.ResourceManager' ) as MockRM , \
186+ patch ('time.sleep' , side_effect = self . get_circuit_breaker ( 5 )) :
159187 spy = MockRM .return_value .open_resource .return_value
160- breaker = self .get_circuit_breaker (5 )
161188 inputs = ['100' , '10' , 'y' ]
162- with patch ('builtins.input' , side_effect = inputs ), \
163- patch ('time.sleep' , side_effect = breaker ):
189+ with patch ('builtins.input' , side_effect = inputs ):
164190 self .run_module_safely ("Keithley_6517B.Pyroelectricity.Backends.Poling_K6517B_Backend_v10" )
165191
166192 def test_10_high_resistance (self ):
167- print ( " \n [SIMULATION] 10. High Resistance K6517B..." )
168- with patch ('pyvisa.ResourceManager' ) as MockRM :
193+ with patch ( 'pyvisa.ResourceManager' ) as MockRM , \
194+ patch ('time.sleep' , side_effect = self . get_circuit_breaker ( 5 )) :
169195 spy = MockRM .return_value .open_resource .return_value
170196 spy .query .return_value = "+1.0E+12,0,0"
171- breaker = self .get_circuit_breaker (5 )
172197 inputs = ['10' , '1' , 'test_file' , 'y' ]
173198 with patch ('builtins.input' , side_effect = inputs ), \
174- patch ('pandas.DataFrame.to_csv' ), \
175- patch ('time.sleep' , side_effect = breaker ):
199+ patch ('pandas.DataFrame.to_csv' ):
176200 try :
177201 self .run_module_safely ("Keithley_6517B.High_Resistance.Backends.IV_K6517B_Simple_Backend_v10" )
178202 except Exception :
179203 pass
180204
181- # =========================================================================
182- # SECTION 3: UTILITY MODULES
183- # =========================================================================
184-
185205 def test_11_gpib_scanner (self ):
186- print ("\n [SIMULATION] 11. GPIB Scanner..." )
187206 with patch ('pyvisa.ResourceManager' ) as MockRM :
188207 rm = MockRM .return_value
189208 rm .list_resources .return_value = ('GPIB0::24::INSTR' ,)
190209 try :
191210 import Utilities .GPIB_Instrument_Scanner_GUI_v4 as scanner
192211 if hasattr (scanner , 'GPIBScannerWindow' ):
193- print (" -> Verified: Import successful" )
212+ print (" -> Verified: Import successful" , flush = True )
194213 except ImportError :
195214 pass
196215
197216 def test_12_gpib_rescue (self ):
198- print ( " \n [SIMULATION] 12. GPIB Rescue..." )
199- with patch ('pyvisa.ResourceManager' ) as MockRM :
217+ with patch ( 'pyvisa.ResourceManager' ) as MockRM , \
218+ patch ('time.sleep' , side_effect = self . get_circuit_breaker ( 3 )) :
200219 rm = MockRM .return_value
201220 rm .list_resources .return_value = ('GPIB0::1::INSTR' ,)
202- breaker = self .get_circuit_breaker (3 )
203- with patch ('time.sleep' , side_effect = breaker ):
204- self .run_module_safely ("Utilities.GPIB_Interface_Rescue_Simple_Backened_v2_" )
221+ self .run_module_safely ("Utilities.GPIB_Interface_Rescue_Simple_Backened_v2_" )
205222
206223if __name__ == '__main__' :
207224 unittest .main ()
0 commit comments