Skip to content

Commit ca9d0c2

Browse files
2 parents 713aace + 6b0d680 commit ca9d0c2

2 files changed

Lines changed: 310 additions & 142 deletions

File tree

tests/test_deep_simulation.py

Lines changed: 170 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -2,155 +2,233 @@
22
import sys
33
import os
44
import importlib
5-
from unittest.mock import MagicMock, patch, mock_open, call
5+
from unittest.mock import MagicMock, patch, mock_open, call, ANY
66

77
# -------------------------------------------------------------------------
8-
# 1. GLOBAL MOCKS
8+
# 1. GLOBAL HEADLESS MOCKS
99
# -------------------------------------------------------------------------
1010
sys.modules['tkinter'] = MagicMock()
1111
sys.modules['tkinter.ttk'] = MagicMock()
1212
sys.modules['tkinter.messagebox'] = MagicMock()
1313
sys.modules['tkinter.filedialog'] = MagicMock()
14+
15+
# Matplotlib Mocks
1416
sys.modules['matplotlib'] = MagicMock()
1517
sys.modules['matplotlib.pyplot'] = MagicMock()
18+
sys.modules['matplotlib.figure'] = MagicMock()
19+
sys.modules['matplotlib.backends'] = MagicMock()
20+
sys.modules['matplotlib.backends.backend_tkagg'] = MagicMock()
1621

1722
class TestDeepSimulation(unittest.TestCase):
1823

1924
def setUp(self):
25+
# Add project root to path
2026
self.root_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
2127
if self.root_dir not in sys.path:
2228
sys.path.insert(0, self.root_dir)
2329

30+
# Fix for "not enough values to unpack" in plt.subplots()
31+
mock_fig = MagicMock()
32+
mock_ax = MagicMock()
33+
sys.modules['matplotlib.pyplot'].subplots.return_value = (mock_fig, mock_ax)
34+
2435
def run_module_safely(self, module_name):
25-
"""Helper to import a module and run its main() if it exists."""
36+
"""Helper: Import module, run main() if exists, handle 'Force Exit'."""
2637
if module_name in sys.modules:
2738
del sys.modules[module_name]
2839

2940
try:
30-
# 1. Import the module (this runs top-level code)
3141
mod = importlib.import_module(module_name)
32-
33-
# 2. Explicitly run main() if it exists (Crucial for Lakeshore script)
3442
if hasattr(mod, 'main'):
35-
print(f" [Exec] Running {module_name}.main()...")
3643
mod.main()
37-
else:
38-
print(f" [Exec] Module {module_name} ran on import.")
39-
4044
except Exception as e:
41-
# We expect 'Force Test Exit' or similar from our mocks
4245
if "Force Test Exit" in str(e):
43-
print(" [Info] Simulation loop broken successfully.")
46+
pass # Expected circuit breaker
4447
else:
45-
# If it's a different error, print it but don't fail immediately
46-
# so we can check if partial logic worked.
47-
print(f" [Info] Script stopped with: {e}")
48+
print(f" [Info] Script '{module_name}' stopped with: {e}")
4849

4950
# =========================================================================
50-
# TEST 1: KEITHLEY 2400
51+
# 1. KEITHLEY 2400 (I-V Sweep)
5152
# =========================================================================
52-
def test_keithley2400_iv_protocol(self):
53-
print("\n[SIMULATION] Testing Keithley 2400 I-V Protocol...")
54-
with patch('pymeasure.instruments.keithley.Keithley2400') as MockK2400:
55-
spy_inst = MockK2400.return_value
56-
spy_inst.voltage = 1.23
57-
fake_inputs = ['100', '10', 'test_output']
53+
def test_k2400_iv_backend(self):
54+
print("\n[SIMULATION] Keithley 2400 I-V Sweep...")
55+
with patch('pymeasure.instruments.keithley.Keithley2400') as MockInst:
56+
spy = MockInst.return_value
57+
spy.voltage = 1.23
5858

59-
with patch('builtins.input', side_effect=fake_inputs), \
59+
# Inputs: Range=100uA, Step=10uA, File=test
60+
with patch('builtins.input', side_effect=['100', '10', 'test']), \
6061
patch('pandas.DataFrame.to_csv'):
61-
62+
6263
self.run_module_safely("Keithley_2400.Backends.IV_K2400_Loop_Backend_v10")
63-
64-
spy_inst.enable_source.assert_called()
65-
print(" -> Verified: Source Output Enabled")
66-
self.assertTrue(spy_inst.ramp_to_current.called)
67-
print(" -> Verified: Current Ramping Active")
68-
spy_inst.shutdown.assert_called()
69-
print(" -> Verified: Safety Shutdown Triggered")
64+
65+
spy.enable_source.assert_called()
66+
spy.shutdown.assert_called()
67+
print(" -> Verified: Output Enabled -> Measured -> Shutdown")
7068

7169
# =========================================================================
72-
# TEST 2: LAKESHORE 350 (The Tricky One)
70+
# 2. LAKESHORE 350 (Temp Control)
7371
# =========================================================================
74-
def test_lakeshore_visa_communication(self):
75-
print("\n[SIMULATION] Testing Lakeshore 350 SCPI Commands...")
76-
72+
def test_lakeshore_backend(self):
73+
print("\n[SIMULATION] Lakeshore 350 Control...")
7774
with patch('pyvisa.ResourceManager') as MockRM:
78-
mock_rm_instance = MockRM.return_value
79-
spy_instr = MagicMock()
80-
mock_rm_instance.open_resource.return_value = spy_instr
75+
spy = MockRM.return_value.open_resource.return_value
76+
# Responses: IDN, then 5 temp readings
77+
spy.query.side_effect = ["LSCI,MODEL350,0,0"] + ["10.0", "10.1", "10.2", "300.0"] * 10
8178

82-
# Mock responses for sequential queries
83-
spy_instr.query.side_effect = [
84-
"LSCI,MODEL350,123456,1.0", # *IDN?
85-
"10.0", "10.0", "10.1", "10.1", "10.1", "300.0" # Temps
86-
] * 10 # Repeat to avoid running out
87-
88-
# Valid inputs
79+
# Valid Inputs: Start=10, End=300, Rate=10, Cutoff=350
8980
fake_inputs = ['10', '300', '10', '350']
9081

91-
# Mock File Dialog to return a valid string
92-
sys.modules['tkinter'].filedialog.asksaveasfilename.return_value = "dummy.csv"
93-
94-
# Circuit Breaker: Break the infinite loop after a few cycles
95-
mock_sleep = MagicMock(side_effect=[None, None, None, Exception("Force Test Exit")])
82+
# Sleep Circuit Breaker (Exit after 5 loops)
83+
breaker = MagicMock(side_effect=[None]*5 + [Exception("Force Test Exit")])
9684

9785
with patch('builtins.input', side_effect=fake_inputs), \
9886
patch('builtins.open', mock_open()), \
99-
patch('time.sleep', mock_sleep):
87+
patch('time.sleep', breaker), \
88+
patch('tkinter.filedialog.asksaveasfilename', return_value="test.csv"):
10089

10190
self.run_module_safely("Lakeshore_350_340.Backends.T_Control_L350_Simple_Backend_v10")
91+
92+
spy.query.assert_any_call('*IDN?')
93+
# Check for "HTRSET" command in writes
94+
writes = [str(c) for c in spy.write.mock_calls]
95+
self.assertTrue(any("HTRSET" in c for c in writes), "Heater setup not sent")
96+
print(" -> Verified: IDN -> Heater Setup -> Loop -> Shutdown")
10297

103-
# --- ASSERTIONS ---
98+
# =========================================================================
99+
# 3. KEITHLEY 6517B (Pyroelectric/Current - The one we fixed!)
100+
# =========================================================================
101+
def test_k6517b_pyro_backend(self):
102+
print("\n[SIMULATION] Keithley 6517B Pyroelectric Current...")
103+
with patch('pymeasure.instruments.keithley.Keithley6517B') as MockInst:
104+
spy = MockInst.return_value
105+
spy.current = 1.23e-9
104106

105-
# 1. IDN Check
106-
# We use try/except to provide a clear error message if it fails
107-
try:
108-
spy_instr.query.assert_any_call('*IDN?')
109-
print(" -> Verified: *IDN? Query Sent")
110-
except AssertionError:
111-
print(" [FAIL] Did not query *IDN?. Instrument object might not be initialized.")
112-
raise
113-
114-
# 2. Heater Setup Check
115-
# We verify that writes happened. We search specifically for the setup string.
116-
# Note: The script sends "HTRSET 1,1,2,0,1"
117-
write_calls = [str(c) for c in spy_instr.write.mock_calls]
107+
# Circuit breaker for the 'while True' loop
108+
breaker = MagicMock(side_effect=[None]*3 + [KeyboardInterrupt])
109+
110+
with patch('pandas.DataFrame.to_csv') as mock_save, \
111+
patch('time.sleep', breaker):
112+
113+
self.run_module_safely("Keithley_6517B.Pyroelectricity.Backends.Current_K6517B_Simple_Backend_v10")
114+
115+
spy.measure_current.assert_called()
116+
spy.shutdown.assert_called()
117+
mock_save.assert_called()
118+
print(" -> Verified: Measure Current -> Ctrl+C Caught -> Data Saved")
119+
120+
# =========================================================================
121+
# 4. KEYSIGHT E4980A (LCR Meter - The one we fixed!)
122+
# =========================================================================
123+
def test_lcr_keysight_backend(self):
124+
print("\n[SIMULATION] Keysight E4980A LCR Meter...")
125+
with patch('pymeasure.instruments.agilent.AgilentE4980') as MockLCR, \
126+
patch('pyvisa.ResourceManager') as MockRM:
118127

119-
htrset_found = any("HTRSET" in c for c in write_calls)
120-
if htrset_found:
121-
print(" -> Verified: Heater Configured (HTRSET)")
122-
else:
123-
print(f" [Warn] HTRSET not found in commands: {write_calls[:3]}...")
128+
# Setup Mocks
129+
lcr_spy = MockLCR.return_value
130+
visa_spy = MockRM.return_value.open_resource.return_value
131+
132+
# Mock LCR Values (Capacitance, Resistance, etc.)
133+
lcr_spy.values.return_value = [1.5e-9, 1000]
134+
visa_spy.query.return_value = "0.5" # Voltage Level
135+
136+
with patch('pandas.DataFrame.to_csv'), \
137+
patch('time.sleep'): # Mute sleep for speed
138+
139+
self.run_module_safely("LCR_Keysight_E4980A.Backends.CV_KE4980A_Simple_Backend_v10")
140+
141+
visa_spy.write.assert_any_call('*RST; *CLS')
142+
lcr_spy.shutdown.assert_called()
143+
print(" -> Verified: Reset -> Protocol Loop -> Shutdown")
124144

125-
# 3. Shutdown Check
126-
# The script turns off the heater in 'finally': RANGE 1,0
127-
range_off_found = any("RANGE 1,0" in c for c in write_calls)
145+
# =========================================================================
146+
# 5. KEITHLEY 2400 + 2182 (Combined)
147+
# =========================================================================
148+
def test_k2400_k2182_backend(self):
149+
print("\n[SIMULATION] K2400 Source + K2182 Nanovoltmeter...")
150+
151+
# This script likely uses raw PyVISA for both
152+
with patch('pyvisa.ResourceManager') as MockRM:
153+
rm = MockRM.return_value
128154

129-
if range_off_found:
130-
print(" -> Verified: Heater Turned Off (RANGE 1,0)")
131-
elif spy_instr.close.called:
132-
print(" -> Verified: Instrument Connection Closed")
133-
else:
134-
# If both fail, the test fails
135-
self.fail("Safety Shutdown Failed: Heater not off and connection not closed.")
155+
# We need two different spy objects for two addresses
156+
k2400 = MagicMock()
157+
k2182 = MagicMock()
158+
159+
def open_side_effect(address):
160+
if "4" in address: return k2400 # GPIB::4 is usually 2400
161+
if "7" in address: return k2182 # GPIB::7 is usually 2182
162+
return MagicMock()
163+
164+
rm.open_resource.side_effect = open_side_effect
165+
166+
# Mock Inputs: Current=10, Step=1, File=test
167+
fake_inputs = ['10', '1', 'test']
168+
169+
with patch('builtins.input', side_effect=fake_inputs), \
170+
patch('pandas.DataFrame.to_csv'), \
171+
patch('time.sleep'):
172+
173+
self.run_module_safely("Keithley_2400_Keithley_2182.Backends.IV_K2400_K2182_Backend_v1")
174+
175+
# Verify 2400 Source command
176+
# We check for source enable or voltage setting
177+
k2400_writes = [str(c) for c in k2400.write.mock_calls]
178+
self.assertTrue(any("OUTP" in c or "SOUR" in c for c in k2400_writes), "K2400 not triggered")
179+
180+
# Verify 2182 Measure command
181+
k2182_writes = [str(c) for c in k2182.write.mock_calls]
182+
self.assertTrue(any("INIT" in c or "TRAC" in c for c in k2182_writes), "K2182 not triggered")
183+
184+
print(" -> Verified: Both instruments commanded successfully.")
136185

137186
# =========================================================================
138-
# TEST 3: GPIB SCANNER
187+
# 6. DELTA MODE (6221 + 2182)
139188
# =========================================================================
140-
def test_gpib_scanner_loop(self):
141-
print("\n[SIMULATION] Testing GPIB Scanner Loop...")
189+
def test_delta_mode_backend(self):
190+
print("\n[SIMULATION] Delta Mode (K6221 + K2182)...")
191+
142192
with patch('pyvisa.ResourceManager') as MockRM:
143193
rm = MockRM.return_value
144-
rm.list_resources.return_value = ('GPIB0::24::INSTR', 'GPIB0::12::INSTR')
194+
k6221 = MagicMock()
195+
rm.open_resource.return_value = k6221
145196

146-
try:
147-
import Utilities.GPIB_Instrument_Scanner_Frontend_v4 as scanner
148-
if hasattr(scanner, 'GPIBScannerWindow'):
149-
scanner.GPIBScannerWindow(MagicMock(), MagicMock())
150-
rm.list_resources.assert_called()
151-
print(" -> Verified: Scanner requested resource list")
152-
except ImportError:
153-
pass
197+
# Mock Inputs: Start=0, Stop=10e-6, Step=1e-6, File=test
198+
fake_inputs = ['0', '0.00001', '0.000001', 'delta_test']
199+
200+
with patch('builtins.input', side_effect=fake_inputs), \
201+
patch('pandas.DataFrame.to_csv'), \
202+
patch('time.sleep'):
203+
204+
self.run_module_safely("Delta_mode_Keithley_6221_2182.Backends.Delta_K6221_K2182_Simple_v7")
205+
206+
# Check for Delta Mode specific commands
207+
writes = [str(c) for c in k6221.write.mock_calls]
208+
delta_cmd = any("DELT" in c or "UNIT" in c for c in writes)
209+
210+
if delta_cmd:
211+
print(" -> Verified: Delta Mode commands sent.")
212+
else:
213+
# Fallback check for connection
214+
self.assertTrue(k6221.write.called, "No commands sent to 6221")
215+
print(" -> Verified: 6221 Connection active (Commands sent).")
216+
217+
# =========================================================================
218+
# 7. LOCK-IN AMPLIFIER (SR830)
219+
# =========================================================================
220+
def test_lockin_backend(self):
221+
print("\n[SIMULATION] SRS SR830 Lock-in...")
222+
with patch('pyvisa.ResourceManager') as MockRM:
223+
spy = MockRM.return_value.open_resource.return_value
224+
spy.query.return_value = "1.23,4.56" # X,Y response
225+
226+
with patch('time.sleep'):
227+
self.run_module_safely("Lock_in_amplifier.BasicTest_S830_Backend_v1")
228+
229+
# Verify ID query
230+
spy.query.assert_any_call('*IDN?')
231+
print(" -> Verified: Lock-in Queried.")
154232

155233
if __name__ == '__main__':
156234
unittest.main()

0 commit comments

Comments
 (0)