Skip to content

Commit 325632d

Browse files
Create test_full_stack_simulation.py.
Trying to test the final full stack programs
1 parent 57c7d68 commit 325632d

1 file changed

Lines changed: 183 additions & 0 deletions

File tree

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
import unittest
2+
import sys
3+
import os
4+
import queue
5+
from unittest.mock import MagicMock, patch, ANY
6+
7+
# -------------------------------------------------------------------------
8+
# 1. MASTER MOCKS (The "Matrix" Simulation)
9+
# We replace the entire physical world (Screen, Instruments, OS) with Mocks.
10+
# -------------------------------------------------------------------------
11+
sys.modules['tkinter'] = MagicMock()
12+
sys.modules['tkinter.ttk'] = MagicMock()
13+
sys.modules['tkinter.messagebox'] = MagicMock()
14+
sys.modules['tkinter.filedialog'] = MagicMock()
15+
sys.modules['matplotlib'] = MagicMock()
16+
sys.modules['matplotlib.pyplot'] = MagicMock()
17+
sys.modules['matplotlib.backends.backend_tkagg'] = MagicMock()
18+
sys.modules['pyvisa'] = MagicMock()
19+
sys.modules['pymeasure'] = MagicMock()
20+
sys.modules['PIL'] = MagicMock()
21+
sys.modules['PIL.Image'] = MagicMock()
22+
sys.modules['PIL.ImageTk'] = MagicMock()
23+
24+
class TestFullStack(unittest.TestCase):
25+
26+
def setUp(self):
27+
# Ensure we can import your scripts
28+
self.root_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
29+
if self.root_dir not in sys.path:
30+
sys.path.insert(0, self.root_dir)
31+
32+
# =========================================================================
33+
# SCENARIO 1: THE LAUNCHER (The "Control Center")
34+
# Goal: Verify that clicking a dashboard button actually launches the correct module.
35+
# =========================================================================
36+
def test_launcher_integration(self):
37+
print("\n[FULL-STACK] Testing PICA Launcher Integration...")
38+
39+
# 1. Import the Launcher
40+
try:
41+
import PICA_v6 as launcher
42+
except ImportError:
43+
self.skipTest("Could not import PICA_v6.py")
44+
45+
# 2. Spy on the multiprocessing.Process class
46+
# We want to know if PICA *attempts* to start a new process
47+
with patch('multiprocessing.Process') as MockProcess:
48+
49+
# 3. Initialize the App
50+
mock_root = MagicMock()
51+
app = launcher.PICALauncherApp(mock_root)
52+
53+
# 4. Simulate clicking the "K2400 I-V" button
54+
# We look up the script path from your dictionary
55+
script_key = "K2400 I-V"
56+
if script_key in app.SCRIPT_PATHS:
57+
target_script = app.SCRIPT_PATHS[script_key]
58+
59+
# Trigger the launch function directly
60+
app.launch_script(target_script)
61+
62+
# 5. ASSERTION: Did a process start?
63+
MockProcess.assert_called()
64+
65+
# 6. DEEP ASSERTION: Was it the CORRECT script?
66+
# Get the arguments passed to Process(target=..., args=(PATH,))
67+
_, kwargs = MockProcess.call_args
68+
captured_args = kwargs.get('args', [])
69+
70+
# We check if the path sent to the process matches the K2400 script
71+
self.assertIn("IV_K2400_Frontend", str(captured_args[0]))
72+
print(f" -> Verified: Launcher correctly targeted '{os.path.basename(str(captured_args[0]))}'")
73+
74+
# Verify start() was called on the process
75+
MockProcess.return_value.start.assert_called()
76+
print(" -> Verified: New Process was spawned successfully.")
77+
78+
# =========================================================================
79+
# SCENARIO 2: THE MEASUREMENT GUI (The "Experiment")
80+
# Goal: Verify 'Start' button -> Launches Backend -> Updates Graph
81+
# =========================================================================
82+
def test_k2400_gui_workflow(self):
83+
print("\n[FULL-STACK] Testing K2400 IV Frontend Workflow...")
84+
85+
# 1. Import the specific frontend module
86+
# Adjust this import path if your folder structure is slightly different!
87+
try:
88+
import Keithley_2400.IV_K2400_Frontend_v5 as frontend
89+
except ImportError:
90+
# Try dynamic import if path is complex
91+
print(" [Note] Standard import failed, trying dynamic...")
92+
import importlib.util
93+
path = os.path.join(self.root_dir, 'Keithley_2400', 'IV_K2400_Frontend_v5.py')
94+
if not os.path.exists(path):
95+
self.skipTest(f"Could not find frontend at {path}")
96+
spec = importlib.util.spec_from_file_location("k2400_fe", path)
97+
frontend = importlib.util.module_from_spec(spec)
98+
sys.modules["k2400_fe"] = frontend
99+
spec.loader.exec_module(frontend)
100+
101+
# 2. Mock the multiprocess parts specific to this module
102+
with patch('multiprocessing.Process') as MockProcess, \
103+
patch('multiprocessing.Queue') as MockQueue:
104+
105+
# Setup the Queue Mock
106+
gui_queue = MagicMock()
107+
MockQueue.return_value = gui_queue
108+
109+
# 3. Initialize the GUI
110+
mock_root = MagicMock()
111+
# Assuming the class is named 'IV_App' or similar based on file name.
112+
# If you renamed the class, we inspect the module to find it.
113+
app_class = None
114+
for name, obj in vars(frontend).items():
115+
if isinstance(obj, type) and "App" in name: # Heuristic to find the main class
116+
app_class = obj
117+
break
118+
119+
if not app_class:
120+
self.skipTest("Could not auto-detect the Main App class in the frontend file.")
121+
122+
print(f" -> Detected App Class: {app_class.__name__}")
123+
app = app_class(mock_root)
124+
125+
# 4. SIMULATE USER INPUT
126+
# We "type" into the Tkinter Entry widgets
127+
# Note: We assume your app stores entries in 'self.entries' dictionary or attributes
128+
# This part tries to be generic:
129+
if hasattr(app, 'entries') and isinstance(app.entries, dict):
130+
# Generic dictionary based form
131+
for key in app.entries:
132+
app.entries[key].insert(0, "10") # Fill everything with 10
133+
elif hasattr(app, 'entry_start'):
134+
# Direct attribute style
135+
app.entry_start.insert(0, "0")
136+
app.entry_end.insert(0, "10")
137+
app.entry_step.insert(0, "1")
138+
139+
# Mock the file dialog so it doesn't pop up
140+
app.file_location_path = "/tmp/test_data"
141+
142+
# 5. CLICK THE START BUTTON
143+
# We verify if the app has a start method
144+
if hasattr(app, 'start_measurement'):
145+
print(" -> Clicking 'Start' button...")
146+
app.start_measurement()
147+
148+
# 6. VERIFY BACKEND LAUNCH
149+
MockProcess.assert_called()
150+
print(" -> Verified: Frontend attempted to launch Backend process.")
151+
152+
# 7. SIMULATE DATA ARRIVAL (The "Full Stack" Loop)
153+
# We manually inject data into the queue that the GUI listens to.
154+
# Format: (Voltage, Current, Time, etc.) -> Depends on your specific unpacking
155+
fake_data = (1.5, 1.23e-6, 0.5) # Fake Volts, Amps, Time
156+
157+
# We force the queue to return this data ONCE, then raise Empty
158+
gui_queue.get_nowait.side_effect = [fake_data, queue.Empty]
159+
gui_queue.empty.side_effect = [False, True] # First not empty, then empty
160+
161+
# 8. TRIGGER GRAPH UPDATE
162+
# Most Tkinter apps have a function like `update_graph` or `process_queue`
163+
# We find it and call it.
164+
update_func = getattr(app, '_process_data_queue', getattr(app, 'update_graph', None))
165+
166+
if update_func:
167+
print(" -> Simulating incoming data from instrument...")
168+
try:
169+
update_func() # Run one cycle of the update loop
170+
print(" -> Verified: GUI accepted data without crashing.")
171+
172+
# Check if plot was updated
173+
# (If you use matplotlib, 'draw' or 'set_data' should be called)
174+
if app.line and hasattr(app.line, 'set_data'):
175+
app.line.set_data.assert_called()
176+
print(" -> Verified: Live Graph updated with new data point.")
177+
except Exception as e:
178+
print(f" [Warn] Graph update hit an error (common in headless): {e}")
179+
else:
180+
print(" [Info] Could not auto-detect queue processing function, skipping data test.")
181+
182+
if __name__ == '__main__':
183+
unittest.main()

0 commit comments

Comments
 (0)