Skip to content

Commit 5a38961

Browse files
test added
1 parent 0e39adf commit 5a38961

4 files changed

Lines changed: 231 additions & 69 deletions

File tree

Update_path.py

Lines changed: 0 additions & 68 deletions
This file was deleted.

requirements.txt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,7 @@ pymeasure
55
pyvisa
66
pyvisa-py
77
gpib-ctypes
8-
pillow
8+
pillow
9+
freezegun
10+
pytest
11+
pytest-cov
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import unittest
2+
from unittest.mock import patch, MagicMock, mock_open
3+
import numpy as np
4+
import pandas as pd
5+
import sys
6+
import os
7+
8+
# Add the root directory to the Python path
9+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
10+
11+
# Now we can import the module to be tested
12+
from Keithley_2400.Backends import IV_K2400_Loop_Backend_v10 as iv_backend
13+
14+
class TestIVK2400LoopBackend(unittest.TestCase):
15+
16+
@patch('builtins.input', side_effect=['10', '2', 'test_output'])
17+
@patch('pymeasure.instruments.keithley.Keithley2400')
18+
@patch('matplotlib.pyplot.show')
19+
@patch('pandas.DataFrame.to_csv')
20+
def test_main_full_run(self, mock_to_csv, mock_plt_show, mock_keithley_class, mock_input):
21+
"""
22+
Test the main function to ensure it runs through the full I-V sweep process.
23+
"""
24+
# --- MOCK SETUP ---
25+
# Mock the instrument instance
26+
mock_keithley_instance = MagicMock()
27+
mock_keithley_class.return_value = mock_keithley_instance
28+
29+
# Simulate the voltage measurement
30+
# Let's return a voltage that is proportional to the current
31+
def voltage_side_effect():
32+
# A simple linear relationship for testing
33+
return mock_keithley_instance.source_current * 10
34+
35+
# We can't directly access the source_current from ramp_to_current,
36+
# so we will use a side effect to track it
37+
latest_current = [0]
38+
def ramp_side_effect(current):
39+
latest_current[0] = current
40+
# Also update the mock's internal state
41+
mock_keithley_instance.source_current = current
42+
43+
mock_keithley_instance.ramp_to_current.side_effect = ramp_side_effect
44+
45+
# When .voltage is accessed, return the calculated value
46+
type(mock_keithley_instance).voltage = unittest.mock.PropertyMock(side_effect=voltage_side_effect)
47+
48+
# --- EXECUTE SCRIPT ---
49+
iv_backend.main()
50+
51+
# --- ASSERTIONS ---
52+
# 1. Check instrument initialization
53+
mock_keithley_class.assert_called_with("GPIB::4")
54+
mock_keithley_instance.disable_buffer.assert_called_once()
55+
56+
# 2. Check instrument configuration
57+
self.assertEqual(mock_keithley_instance.source_mode, 'current')
58+
self.assertEqual(mock_keithley_instance.source_current_range, 1e-6)
59+
self.assertEqual(mock_keithley_instance.compliance_voltage, 210)
60+
mock_keithley_instance.enable_source.assert_called_once()
61+
mock_keithley_instance.measure_voltage.assert_called_once()
62+
63+
# 3. Check the ramping logic
64+
# Based on input: I_range=10, I_step=2.
65+
# np.linspace(0, 10, int(10/2) + 1) -> linspace(0, 10, 6)
66+
# The values will be [0., 2., 4., 6., 8., 10.]
67+
expected_currents_uA = [0., 2., 4., 6., 8., 10.]
68+
expected_calls = [unittest.mock.call(c * 1e-6) for c in expected_currents_uA]
69+
mock_keithley_instance.ramp_to_current.assert_has_calls(expected_calls)
70+
71+
# 4. Verify the number of measurements taken
72+
self.assertEqual(mock_keithley_instance.ramp_to_current.call_count, 6)
73+
# Voltage is read once per loop
74+
self.assertEqual(type(mock_keithley_instance).voltage.call_count, 6)
75+
76+
# 5. Check data saving
77+
self.assertEqual(mock_to_csv.call_count, 1)
78+
# Check the path construction
79+
call_args = mock_to_csv.call_args
80+
save_path = call_args[0][0]
81+
self.assertIn('test_output.txt', save_path)
82+
83+
# 6. Check that the instrument was shut down
84+
mock_keithley_instance.shutdown.assert_called_once()
85+
86+
# 7. Check that the plot was displayed
87+
mock_plt_show.assert_called_once()
88+
89+
if __name__ == '__main__':
90+
unittest.main()
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import unittest
2+
from unittest.mock import patch, MagicMock, mock_open
3+
import pyvisa
4+
import time
5+
import sys
6+
import os
7+
8+
# Add the root directory to the Python path
9+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
10+
11+
from Lakeshore_350_340.Backends.T_Control_L350_Simple_Backend_v10 import Lakeshore350, get_user_parameters, main
12+
13+
class TestLakeshore350Class(unittest.TestCase):
14+
@patch('pyvisa.ResourceManager')
15+
def setUp(self, mock_rm):
16+
# Mock the entire pyvisa resource manager and the instrument it returns
17+
self.mock_instrument = MagicMock()
18+
mock_rm.return_value.open_resource.return_value = self.mock_instrument
19+
self.mock_instrument.query.return_value = "LSCI,MODEL350,12345,1.0"
20+
21+
# Instantiate the class, which will now use the mock instrument
22+
self.controller = Lakeshore350("GPIB0::13::INSTR")
23+
# Keep a reference to the mock for assertions
24+
self.controller.instrument = self.mock_instrument
25+
26+
def test_initialization_success(self):
27+
# Test that the instrument is initialized and queried
28+
self.mock_instrument.query.assert_called_with('*IDN?')
29+
self.assertIsNotNone(self.controller.instrument)
30+
31+
@patch('pyvisa.ResourceManager')
32+
def test_initialization_failure(self, mock_rm):
33+
# Test that a connection error is raised if pyvisa fails
34+
mock_rm.return_value.open_resource.side_effect = pyvisa.errors.VisaIOError("Test Error")
35+
with self.assertRaises(ConnectionError):
36+
Lakeshore350("GPIB0::13::INSTR")
37+
38+
def test_reset_and_clear(self):
39+
with patch('time.sleep') as mock_sleep:
40+
self.controller.reset_and_clear()
41+
self.mock_instrument.write.assert_any_call('*RST')
42+
self.mock_instrument.write.assert_any_call('*CLS')
43+
self.assertEqual(mock_sleep.call_count, 2)
44+
45+
def test_setup_heater(self):
46+
self.controller.setup_heater(1, 1, 2)
47+
self.mock_instrument.write.assert_called_with('HTRSET 1,1,2,0,1')
48+
49+
def test_setup_ramp(self):
50+
self.controller.setup_ramp(1, 10.0, ramp_on=True)
51+
self.mock_instrument.write.assert_called_with('RAMP 1,1,10.0')
52+
53+
def test_set_setpoint(self):
54+
self.controller.set_setpoint(1, 150.0)
55+
self.mock_instrument.write.assert_called_with('SETP 1,150.0')
56+
57+
def test_set_heater_range(self):
58+
self.controller.set_heater_range(1, 'high')
59+
self.mock_instrument.write.assert_called_with('RANGE 1,5')
60+
61+
def test_get_temperature(self):
62+
self.mock_instrument.query.return_value = "300.123"
63+
temp = self.controller.get_temperature('A')
64+
self.mock_instrument.query.assert_called_with('KRDG? A')
65+
self.assertAlmostEqual(temp, 300.123)
66+
67+
def test_get_heater_output(self):
68+
self.mock_instrument.query.return_value = "50.5"
69+
output = self.controller.get_heater_output(1)
70+
self.mock_instrument.query.assert_called_with('HTR? 1')
71+
self.assertAlmostEqual(output, 50.5)
72+
73+
def test_close(self):
74+
self.controller.close()
75+
# Checks that heater is turned off
76+
self.mock_instrument.write.assert_called_with('RANGE 1,0')
77+
# Checks that the instrument connection is closed
78+
self.mock_instrument.close.assert_called_once()
79+
self.assertIsNone(self.controller.instrument)
80+
81+
class TestMainFunctionAndUserInput(unittest.TestCase):
82+
@patch('builtins.input', side_effect=['100', '200', '300', '10', 'not-a-number', '50', '350', '400'])
83+
def test_get_user_parameters(self, mock_input):
84+
# First call: Valid input
85+
start, end, rate, cutoff = get_user_parameters()
86+
self.assertEqual((start, end, cutoff), (100, 200, 300))
87+
self.assertEqual(rate, 10)
88+
89+
# Second call: Invalid text input, should retry and get the next valid ones
90+
start, end, rate, cutoff = get_user_parameters()
91+
self.assertEqual((start, end, cutoff), (50, 350, 400))
92+
93+
@patch('tkinter.filedialog.asksaveasfilename', return_value='test.csv')
94+
@patch('builtins.input', side_effect=['10', '20', '5', '30'])
95+
@patch('Lakeshore_350_340.Backends.T_Control_L350_Simple_Backend_v10.Lakeshore350')
96+
@patch('matplotlib.pyplot.show')
97+
@patch('builtins.open', new_callable=mock_open)
98+
@patch('time.sleep', MagicMock())
99+
@patch('time.time', side_effect=[1000, 1002, 1004, 1006, 1008, 1010]) # Simulate time passing
100+
def test_main_runs_and_completes(self, mock_time, mock_open_file, mock_plt_show, mock_ls_class, mock_input, mock_file_dialog):
101+
# --- MOCK SETUP ---
102+
mock_controller = MagicMock()
103+
mock_ls_class.return_value = mock_controller
104+
105+
# Simulate temperature readings to control the loop
106+
# Start at 10K, then ramp up to 21K to finish
107+
mock_controller.get_temperature.side_effect = [10.0, 10.0, 10.0, 15.0, 21.0]
108+
mock_controller.get_heater_output.return_value = 25.0
109+
110+
# --- RUN ---
111+
main()
112+
113+
# --- ASSERTIONS ---
114+
# Check initialization
115+
mock_ls_class.assert_called_once()
116+
mock_controller.reset_and_clear.assert_called_once()
117+
mock_controller.setup_heater.assert_called_once()
118+
119+
# Check stabilization loop
120+
mock_controller.set_setpoint.assert_any_call(1, 10) # Set to start temp
121+
122+
# Check main ramp loop
123+
mock_controller.set_setpoint.assert_any_call(1, 20) # Set to end temp
124+
self.assertTrue(mock_controller.get_temperature.call_count >= 3)
125+
126+
# Check file writing
127+
mock_open_file.assert_called_with('test.csv', 'a', newline='')
128+
handle = mock_open_file()
129+
# Header + 3 data rows
130+
self.assertEqual(handle.write.call_count, 1) # only one writer created
131+
132+
# Check shutdown
133+
mock_controller.close.assert_called_once()
134+
mock_plt_show.assert_called_once()
135+
136+
if __name__ == '__main__':
137+
unittest.main()

0 commit comments

Comments
 (0)