|
2 | 2 | import sys |
3 | 3 | import os |
4 | 4 | import importlib |
5 | | -from unittest.mock import MagicMock, patch, mock_open, call |
| 5 | +from unittest.mock import MagicMock, patch, mock_open, call, ANY |
6 | 6 |
|
7 | 7 | # ------------------------------------------------------------------------- |
8 | | -# 1. GLOBAL MOCKS |
| 8 | +# 1. GLOBAL HEADLESS MOCKS |
| 9 | +# We mock the entire physical world so tests run on GitHub servers. |
9 | 10 | # ------------------------------------------------------------------------- |
10 | 11 | sys.modules['tkinter'] = MagicMock() |
11 | 12 | sys.modules['tkinter.ttk'] = MagicMock() |
|
22 | 23 | class TestDeepSimulation(unittest.TestCase): |
23 | 24 |
|
24 | 25 | def setUp(self): |
| 26 | + # Add project root to path so we can import your scripts |
25 | 27 | self.root_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) |
26 | 28 | if self.root_dir not in sys.path: |
27 | 29 | sys.path.insert(0, self.root_dir) |
28 | 30 |
|
29 | | - # --- FIX FOR UNPACKING ERROR --- |
| 31 | + # --- CRITICAL FIX FOR "not enough values to unpack" --- |
| 32 | + # Many scripts call: fig, ax = plt.subplots() |
| 33 | + # We tell the mock to return exactly two items. |
30 | 34 | mock_fig = MagicMock() |
31 | 35 | mock_ax = MagicMock() |
32 | | - # Force plt.subplots() to return a tuple of (figure, axis) |
33 | 36 | sys.modules['matplotlib.pyplot'].subplots.return_value = (mock_fig, mock_ax) |
34 | 37 |
|
35 | 38 | def run_module_safely(self, module_name): |
| 39 | + """Helper: Import module, run main() if exists, handle 'Force Exit'.""" |
36 | 40 | if module_name in sys.modules: |
37 | 41 | del sys.modules[module_name] |
| 42 | + |
38 | 43 | try: |
39 | 44 | mod = importlib.import_module(module_name) |
40 | 45 | if hasattr(mod, 'main'): |
41 | 46 | mod.main() |
42 | 47 | except Exception as e: |
43 | | - if "Force Test Exit" in str(e): |
44 | | - pass |
| 48 | + if "Force Test Exit" in str(e) or isinstance(e, KeyboardInterrupt): |
| 49 | + pass # Expected circuit breaker |
45 | 50 | else: |
46 | 51 | print(f" [Info] Script '{module_name}' stopped with: {e}") |
47 | 52 |
|
48 | | - def test_keithley2400_iv_protocol(self): |
49 | | - print("\n[SIMULATION] Keithley 2400 I-V Protocol...") |
50 | | - with patch('pymeasure.instruments.keithley.Keithley2400') as MockK2400: |
51 | | - spy_inst = MockK2400.return_value |
52 | | - spy_inst.voltage = 1.23 |
53 | | - with patch('builtins.input', side_effect=['100', '10', 'test_output']), \ |
| 53 | + # ========================================================================= |
| 54 | + # 1. KEITHLEY 2400 (I-V Sweep) |
| 55 | + # ========================================================================= |
| 56 | + def test_k2400_iv_backend(self): |
| 57 | + print("\n[SIMULATION] Keithley 2400 I-V Sweep...") |
| 58 | + with patch('pymeasure.instruments.keithley.Keithley2400') as MockInst: |
| 59 | + spy = MockInst.return_value |
| 60 | + spy.voltage = 1.23 |
| 61 | + |
| 62 | + with patch('builtins.input', side_effect=['100', '10', 'test']), \ |
54 | 63 | patch('pandas.DataFrame.to_csv'): |
| 64 | + |
55 | 65 | self.run_module_safely("Keithley_2400.Backends.IV_K2400_Loop_Backend_v10") |
56 | | - spy_inst.enable_source.assert_called() |
57 | | - print(" -> Verified: Source Output Enabled") |
| 66 | + |
| 67 | + spy.enable_source.assert_called() |
| 68 | + spy.shutdown.assert_called() |
| 69 | + print(" -> Verified: Output Enabled -> Measured -> Shutdown") |
58 | 70 |
|
59 | | - def test_lakeshore_visa_communication(self): |
60 | | - print("\n[SIMULATION] Lakeshore 350 SCPI Commands...") |
| 71 | + # ========================================================================= |
| 72 | + # 2. LAKESHORE 350 (Temp Control) |
| 73 | + # ========================================================================= |
| 74 | + def test_lakeshore_backend(self): |
| 75 | + print("\n[SIMULATION] Lakeshore 350 Control...") |
61 | 76 | with patch('pyvisa.ResourceManager') as MockRM: |
62 | | - spy_instr = MockRM.return_value.open_resource.return_value |
63 | | - spy_instr.query.side_effect = ["LSCI,MODEL350,0,0"] + ["10.0"] * 20 # Temp readings |
| 77 | + spy = MockRM.return_value.open_resource.return_value |
| 78 | + spy.query.side_effect = ["LSCI,MODEL350,0,0"] + ["10.0", "10.1", "10.2", "300.0"] * 10 |
64 | 79 |
|
65 | | - # Inputs: Start=10, End=300, Rate=10, Cutoff=350 |
| 80 | + # Valid Inputs: Start=10, End=300, Rate=10, Cutoff=350 |
66 | 81 | fake_inputs = ['10', '300', '10', '350'] |
67 | | - sys.modules['tkinter'].filedialog.asksaveasfilename.return_value = "dummy.csv" |
68 | 82 |
|
69 | | - # Circuit Breaker |
70 | | - mock_sleep = MagicMock(side_effect=[None]*5 + [Exception("Force Test Exit")]) |
| 83 | + # Sleep Circuit Breaker |
| 84 | + breaker = MagicMock(side_effect=[None]*5 + [Exception("Force Test Exit")]) |
71 | 85 |
|
72 | 86 | with patch('builtins.input', side_effect=fake_inputs), \ |
73 | 87 | patch('builtins.open', mock_open()), \ |
74 | | - patch('time.sleep', mock_sleep): |
| 88 | + patch('time.sleep', breaker), \ |
| 89 | + patch('tkinter.filedialog.asksaveasfilename', return_value="test.csv"): |
75 | 90 |
|
76 | 91 | self.run_module_safely("Lakeshore_350_340.Backends.T_Control_L350_Simple_Backend_v10") |
| 92 | + |
| 93 | + spy.query.assert_any_call('*IDN?') |
| 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: Heater Configured (HTRSET)") |
77 | 97 |
|
78 | | - try: |
79 | | - spy_instr.query.assert_any_call('*IDN?') |
80 | | - print(" -> Verified: *IDN? Query Sent") |
81 | | - except AssertionError: |
82 | | - print(" [FAIL] IDN Query missed.") |
| 98 | + # ========================================================================= |
| 99 | + # 3. KEITHLEY 6517B (Pyroelectric Current) |
| 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 |
| 106 | + |
| 107 | + # Circuit breaker for 'while True' |
| 108 | + breaker = MagicMock(side_effect=[None]*3 + [KeyboardInterrupt]) |
83 | 109 |
|
84 | | - write_calls = [str(c) for c in spy_instr.write.mock_calls] |
85 | | - if any("HTRSET" in c for c in write_calls): |
86 | | - print(" -> Verified: Heater Configured (HTRSET)") |
| 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 | + print(" -> Verified: Measure Current -> Ctrl+C Caught -> Shutdown") |
| 118 | + |
| 119 | + # ========================================================================= |
| 120 | + # 4. KEYSIGHT E4980A (LCR Meter) |
| 121 | + # ========================================================================= |
| 122 | + def test_lcr_keysight_backend(self): |
| 123 | + print("\n[SIMULATION] Keysight E4980A LCR Meter...") |
| 124 | + with patch('pymeasure.instruments.agilent.AgilentE4980') as MockLCR, \ |
| 125 | + patch('pyvisa.ResourceManager') as MockRM: |
| 126 | + |
| 127 | + lcr_spy = MockLCR.return_value |
| 128 | + visa_spy = MockRM.return_value.open_resource.return_value |
| 129 | + |
| 130 | + # Mock Values: [Capacitance, Resistance] |
| 131 | + lcr_spy.values.return_value = [1.5e-9, 1000] |
| 132 | + visa_spy.query.return_value = "0.5" # Voltage Level |
| 133 | + |
| 134 | + with patch('pandas.DataFrame.to_csv'), \ |
| 135 | + patch('time.sleep'): # Mute sleep for speed |
| 136 | + |
| 137 | + self.run_module_safely("LCR_Keysight_E4980A.Backends.CV_KE4980A_Simple_Backend_v10") |
| 138 | + |
| 139 | + visa_spy.write.assert_any_call('*RST; *CLS') |
| 140 | + lcr_spy.shutdown.assert_called() |
| 141 | + print(" -> Verified: Reset -> Protocol Loop -> Shutdown") |
| 142 | + |
| 143 | + # ========================================================================= |
| 144 | + # 5. DELTA MODE (K6221 + K2182) |
| 145 | + # ========================================================================= |
| 146 | + def test_delta_mode_backend(self): |
| 147 | + print("\n[SIMULATION] Delta Mode (K6221 + K2182)...") |
| 148 | + with patch('pyvisa.ResourceManager') as MockRM: |
| 149 | + k6221 = MagicMock() |
| 150 | + MockRM.return_value.open_resource.return_value = k6221 |
| 151 | + |
| 152 | + # Inputs: Start=0, Stop=10e-6, Step=1e-6, File=test |
| 153 | + fake_inputs = ['0', '0.00001', '0.000001', 'delta_test'] |
| 154 | + |
| 155 | + with patch('builtins.input', side_effect=fake_inputs), \ |
| 156 | + patch('pandas.DataFrame.to_csv'), \ |
| 157 | + patch('time.sleep'): |
| 158 | + |
| 159 | + self.run_module_safely("Delta_mode_Keithley_6221_2182.Backends.Delta_K6221_K2182_Simple_v7") |
| 160 | + |
| 161 | + self.assertTrue(k6221.write.called) |
| 162 | + print(" -> Verified: Commands sent to K6221/K2182") |
| 163 | + |
| 164 | + # ========================================================================= |
| 165 | + # 6. LOCK-IN AMPLIFIER (SR830) |
| 166 | + # ========================================================================= |
| 167 | + def test_lockin_backend(self): |
| 168 | + print("\n[SIMULATION] SRS SR830 Lock-in...") |
| 169 | + with patch('pyvisa.ResourceManager') as MockRM: |
| 170 | + spy = MockRM.return_value.open_resource.return_value |
| 171 | + spy.query.return_value = "1.23,4.56" # Mock X,Y response |
| 172 | + |
| 173 | + with patch('time.sleep'): |
| 174 | + self.run_module_safely("Lock_in_amplifier.BasicTest_S830_Backend_v1") |
| 175 | + |
| 176 | + spy.query.assert_any_call('*IDN?') |
| 177 | + print(" -> Verified: Lock-in IDN Queried") |
| 178 | + |
| 179 | + # ========================================================================= |
| 180 | + # 7. COMBINED K2400 + K2182 |
| 181 | + # ========================================================================= |
| 182 | + def test_k2400_k2182_backend(self): |
| 183 | + print("\n[SIMULATION] Combined K2400 + K2182...") |
| 184 | + with patch('pyvisa.ResourceManager') as MockRM: |
| 185 | + rm = MockRM.return_value |
| 186 | + # We mocking opening 2 different resources |
| 187 | + k2400 = MagicMock() |
| 188 | + k2182 = MagicMock() |
| 189 | + rm.open_resource.side_effect = [k2400, k2182] # First call 2400, second 2182 |
| 190 | + |
| 191 | + fake_inputs = ['10', '1', 'test'] # Current, Step, File |
| 192 | + |
| 193 | + with patch('builtins.input', side_effect=fake_inputs), \ |
| 194 | + patch('pandas.DataFrame.to_csv'), \ |
| 195 | + patch('time.sleep'): |
| 196 | + |
| 197 | + self.run_module_safely("Keithley_2400_Keithley_2182.Backends.IV_K2400_K2182_Backend_v1") |
| 198 | + |
| 199 | + print(" -> Verified: Multi-instrument script executed.") |
87 | 200 |
|
88 | | - def test_gpib_scanner_loop(self): |
89 | | - print("\n[SIMULATION] Testing GPIB Scanner Loop...") |
| 201 | + # ========================================================================= |
| 202 | + # 8. KEITHLEY 6517B (High Resistance I-V) |
| 203 | + # ========================================================================= |
| 204 | + def test_k6517b_high_res(self): |
| 205 | + print("\n[SIMULATION] Keithley 6517B High Resistance...") |
| 206 | + with patch('pyvisa.ResourceManager') as MockRM: |
| 207 | + spy = MockRM.return_value.open_resource.return_value |
| 208 | + spy.query.return_value = "+1.23E-12" # Current reading |
| 209 | + |
| 210 | + fake_inputs = ['10', '1', 'test'] # Voltage, Step, File |
| 211 | + |
| 212 | + with patch('builtins.input', side_effect=fake_inputs), \ |
| 213 | + patch('pandas.DataFrame.to_csv'), \ |
| 214 | + patch('time.sleep'): |
| 215 | + |
| 216 | + self.run_module_safely("Keithley_6517B.High_Resistance.Backends.IV_K6517B_Simple_Backend_v10") |
| 217 | + |
| 218 | + # Check if voltage was applied |
| 219 | + writes = [str(c) for c in spy.write.mock_calls] |
| 220 | + self.assertTrue(any("SOUR:VOLT" in c for c in writes), "Voltage Source not set") |
| 221 | + print(" -> Verified: Voltage Source Commands Sent") |
| 222 | + |
| 223 | + # ========================================================================= |
| 224 | + # 9. POLING K6517B |
| 225 | + # ========================================================================= |
| 226 | + def test_k6517b_poling(self): |
| 227 | + print("\n[SIMULATION] Keithley 6517B Poling...") |
| 228 | + with patch('pyvisa.ResourceManager') as MockRM: |
| 229 | + spy = MockRM.return_value.open_resource.return_value |
| 230 | + |
| 231 | + # Inputs: Voltage=100, Time=10 |
| 232 | + fake_inputs = ['100', '10'] |
| 233 | + |
| 234 | + with patch('builtins.input', side_effect=fake_inputs), \ |
| 235 | + patch('time.sleep'): |
| 236 | + |
| 237 | + self.run_module_safely("Keithley_6517B.Pyroelectricity.Backends.Poling_K6517B_Backend_v10") |
| 238 | + |
| 239 | + # Check for output enable |
| 240 | + writes = [str(c) for c in spy.write.mock_calls] |
| 241 | + if any("OPER" in c or "ON" in c for c in writes): |
| 242 | + print(" -> Verified: Poling Voltage Enabled") |
| 243 | + |
| 244 | + # ========================================================================= |
| 245 | + # 10. GPIB SCANNER (Utility) |
| 246 | + # ========================================================================= |
| 247 | + def test_gpib_scanner(self): |
| 248 | + print("\n[SIMULATION] GPIB Scanner Utility...") |
90 | 249 | with patch('pyvisa.ResourceManager') as MockRM: |
91 | 250 | rm = MockRM.return_value |
92 | | - rm.list_resources.return_value = ('GPIB0::24::INSTR', 'GPIB0::12::INSTR') |
| 251 | + rm.list_resources.return_value = ('GPIB0::24::INSTR',) |
| 252 | + |
93 | 253 | try: |
94 | 254 | import Utilities.GPIB_Instrument_Scanner_Frontend_v4 as scanner |
95 | 255 | if hasattr(scanner, 'GPIBScannerWindow'): |
96 | 256 | scanner.GPIBScannerWindow(MagicMock(), MagicMock()) |
97 | 257 | rm.list_resources.assert_called() |
98 | | - print(" -> Verified: Scanner requested resource list") |
| 258 | + print(" -> Verified: Scanner listed resources") |
99 | 259 | except ImportError: |
100 | 260 | pass |
101 | 261 |
|
|
0 commit comments