|
| 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