|
| 1 | +import unittest |
| 2 | +import sys |
| 3 | +import os |
| 4 | +import importlib.util |
| 5 | +from unittest.mock import MagicMock, patch |
| 6 | + |
| 7 | +# ------------------------------------------------------------------------- |
| 8 | +# GLOBAL HEADLESS MOCKS |
| 9 | +# We mock everything before we even look for files. |
| 10 | +# ------------------------------------------------------------------------- |
| 11 | +MOCK_MODULES = [ |
| 12 | + 'tkinter', 'tkinter.ttk', 'tkinter.messagebox', 'tkinter.filedialog', |
| 13 | + 'tkinter.scrolledtext', 'tkinter.font', 'tkinter.Canvas', |
| 14 | + 'pyvisa', 'pyvisa.errors', 'pyvisa.resources', |
| 15 | + 'pymeasure', 'pymeasure.instruments', 'pymeasure.instruments.keithley', |
| 16 | + 'matplotlib', 'matplotlib.pyplot', 'matplotlib.backends', |
| 17 | + 'matplotlib.backends.backend_tkagg', 'matplotlib.figure', |
| 18 | + 'PIL', 'PIL.Image', 'PIL.ImageTk', |
| 19 | + 'pandas', 'numpy', 'gpib_ctypes' |
| 20 | +] |
| 21 | + |
| 22 | +for mod in MOCK_MODULES: |
| 23 | + sys.modules[mod] = MagicMock() |
| 24 | + |
| 25 | +class TestDynamicDiscovery(unittest.TestCase): |
| 26 | + |
| 27 | + def setUp(self): |
| 28 | + # Point to the root directory of the repo |
| 29 | + self.root_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) |
| 30 | + |
| 31 | + def load_module_from_path(self, file_path): |
| 32 | + """ |
| 33 | + Dynamically loads a Python file as a module given its path. |
| 34 | + This effectively 'runs' the top-level code (imports, class definitions). |
| 35 | + """ |
| 36 | + spec = importlib.util.spec_from_file_location("dynamic_module", file_path) |
| 37 | + if spec and spec.loader: |
| 38 | + module = importlib.util.module_from_spec(spec) |
| 39 | + sys.modules["dynamic_module"] = module |
| 40 | + spec.loader.exec_module(module) |
| 41 | + return module |
| 42 | + return None |
| 43 | + |
| 44 | + def test_all_frontends_and_launchers(self): |
| 45 | + """ |
| 46 | + Walks through the entire repository. |
| 47 | + If a file looks like a GUI script, try to import it. |
| 48 | + """ |
| 49 | + print("\n[AUTO-DISCOVERY] Scanning repository for GUI modules...") |
| 50 | + |
| 51 | + found_count = 0 |
| 52 | + failure_count = 0 |
| 53 | + |
| 54 | + # Walk through all folders starting from root |
| 55 | + for root, dirs, files in os.walk(self.root_dir): |
| 56 | + |
| 57 | + # Skip the 'tests', 'Setup', and hidden folders |
| 58 | + if "tests" in root or "Setup" in root or ".__" in root: |
| 59 | + continue |
| 60 | + |
| 61 | + for file in files: |
| 62 | + # DEFINITION OF A GUI MODULE: |
| 63 | + # 1. Ends with .py |
| 64 | + # 2. Contains 'Frontend' OR starts with 'PICA_v' (your launcher) |
| 65 | + if file.endswith(".py") and ("Frontend" in file or file.startswith("PICA_v")): |
| 66 | + |
| 67 | + full_path = os.path.join(root, file) |
| 68 | + rel_path = os.path.relpath(full_path, self.root_dir) |
| 69 | + |
| 70 | + print(f" -> Testing: {rel_path}", end=" ... ") |
| 71 | + |
| 72 | + try: |
| 73 | + # Try to load it |
| 74 | + self.load_module_from_path(full_path) |
| 75 | + print("OK") |
| 76 | + found_count += 1 |
| 77 | + except Exception as e: |
| 78 | + print(f"FAIL\n Error: {e}") |
| 79 | + failure_count += 1 |
| 80 | + # We don't fail immediately so we can see all errors |
| 81 | + # But we record it to fail the test at the end |
| 82 | + |
| 83 | + print(f"\n[SUMMARY] Tested {found_count} modules. Failed {failure_count}.") |
| 84 | + |
| 85 | + # Fail the test if anything broke |
| 86 | + self.assertEqual(failure_count, 0, f"{failure_count} modules failed to load.") |
| 87 | + |
| 88 | +if __name__ == '__main__': |
| 89 | + unittest.main() |
0 commit comments