Skip to content

Commit bb3629d

Browse files
committed
Add factory to create GUI independent of running it.
Add unittests. Fix imports to avoid cross contamination between running engines. Pyright and unittest in Github Actions workflow.
1 parent 72d7921 commit bb3629d

3 files changed

Lines changed: 160 additions & 11 deletions

File tree

.github/workflows/build_exe.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,13 @@ jobs:
1919
with:
2020
python-version: "3.13"
2121
- name: Install dependencies
22-
run: python -m pip install --upgrade pip pyinstaller
22+
run: python -m pip install --upgrade pip pyinstaller pyright
2323
- name: Install Open Pectus Engine Manager
2424
run: pip install -e .
25+
- name: Type check with pyright
26+
run: pyright
27+
- name: Test with unittest
28+
run: python -m unittest
2529
- name: Build .exe
2630
run: pyinstaller pyinstaller.spec
2731
- name: Zip application

openpectus_engine_manager_gui/__init__.py

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
"""Run multiple [Open Pectus](https://github.com/Open-Pectus/Open-Pectus/)
22
engines in a convenient user interface."""
3+
import sys
4+
INITIAL_MODULES = sys.modules.keys()
35
import asyncio
46
from collections import defaultdict
57
import ctypes
68
import json
79
import logging
8-
from logging.handlers import RotatingFileHandler
910
import os
1011
import platform
1112
import ssl
12-
import sys
1313
import threading
1414
import time
1515
import tkinter as tk
@@ -24,6 +24,7 @@
2424
import httpx
2525
import pystray
2626
import multiprocess
27+
import multiprocess.spawn
2728

2829
__version__ = "0.1.0"
2930
# This application is written for Windows
@@ -119,14 +120,14 @@ class LogRecorder(logging.Handler):
119120
def __init__(self, *args, **kwargs):
120121
super().__init__(*args, **kwargs)
121122
self.logs: Dict[str, List[logging.LogRecord]] = defaultdict(list)
122-
self.emit_callback: List[Callable] = []
123+
self.emit_callbacks: List[Callable] = []
123124
self.engine_names: List[str] = []
124125

125126
def emit(self, record: logging.LogRecord):
126127
assert record.threadName is not None
127128
if record.threadName in self.engine_names:
128129
self.logs[record.threadName].append(record)
129-
for fn in self.emit_callback:
130+
for fn in self.emit_callbacks:
130131
fn(record, record.threadName)
131132

132133
def clear_log(self, thread_name):
@@ -172,7 +173,7 @@ async def start_engine(loop: asyncio.EventLoop):
172173
# so don't let threads manipulate it at the same time.
173174
import_lock.acquire()
174175
for k in list(sys.modules.keys()):
175-
if "openpectus" in k:
176+
if k not in INITIAL_MODULES:
176177
del sys.modules[k]
177178
from openpectus.engine.main import create_uod
178179
from openpectus.engine.engine import Engine
@@ -185,6 +186,9 @@ async def start_engine(loop: asyncio.EventLoop):
185186
from openpectus.engine.engine_runner import EngineRunner
186187
from openpectus.protocol.engine_dispatcher import EngineDispatcher
187188
from openpectus.lang.exec.tags import SystemTagName
189+
import logging
190+
import os
191+
from logging.handlers import RotatingFileHandler
188192
import_lock.release()
189193
# Attach log recorder to Open Pectus loggers created on import
190194
# to catch them and show them in EngineOutput
@@ -297,6 +301,7 @@ async def cancel():
297301
cancel(),
298302
self.loops[engine_name]
299303
)
304+
self.loops[engine_name].stop()
300305

301306
def validate_engine(self, engine_item: Dict[str, str]):
302307
engine_name = engine_item["engine_name"]
@@ -306,9 +311,12 @@ def validate():
306311
# Remove cached modules to avoid cross-contamination
307312
import_lock.acquire()
308313
for k in list(sys.modules.keys()).copy():
309-
if "openpectus" in k:
314+
if k not in INITIAL_MODULES:
310315
del sys.modules[k]
311316
from openpectus.engine.main import validate_and_exit
317+
import logging
318+
import os
319+
from logging.handlers import RotatingFileHandler
312320
import_lock.release()
313321
# Attach log recorder to Open Pectus logs to catch them
314322
# and show them in EngineOutput
@@ -1098,7 +1106,7 @@ def _about(self):
10981106
)
10991107

11001108

1101-
def main():
1109+
def assemble_gui() -> OpenPectusEngineManagerGui:
11021110
# Instantiate objects
11031111
persistent_data = PersistentData()
11041112
gui = OpenPectusEngineManagerGui(persistent_data)
@@ -1116,8 +1124,8 @@ def main():
11161124
gui.engine_list.on_validate_callback.append(engine_manager.validate_engine)
11171125
gui.add_engine_callback.append(lambda x: log_recorder.engine_names.append(x))
11181126
gui.remove_engine_callback.append(lambda x: log_recorder.engine_names.remove(x))
1119-
log_recorder.emit_callback.append(gui.engine_list.set_tag_for_engine_name)
1120-
log_recorder.emit_callback.append(
1127+
log_recorder.emit_callbacks.append(gui.engine_list.set_tag_for_engine_name)
1128+
log_recorder.emit_callbacks.append(
11211129
gui.engine_output.insert_log_record_for_engine
11221130
)
11231131
# Override methods
@@ -1127,10 +1135,15 @@ def main():
11271135
# Populate GUI with persistent data
11281136
for uod_filename in persistent_data["uods"]:
11291137
gui.load_uod_file(uod_filename)
1138+
return gui
1139+
1140+
1141+
def main():
1142+
gui = assemble_gui()
11301143
# Start Tk event loop
11311144
gui.mainloop()
11321145

11331146

11341147
if __name__ == "__main__":
1135-
multiprocess.freeze_support()
1148+
multiprocess.spawn.freeze_support()
11361149
main()
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import unittest
2+
import os
3+
import time
4+
from logging.handlers import BufferingHandler
5+
from typing import List
6+
7+
import openpectus_engine_manager_gui
8+
from openpectus.engine.configuration import demo_uod
9+
10+
gui = openpectus_engine_manager_gui.assemble_gui()
11+
gui.icon.stop()
12+
13+
14+
class TestPersistentData(unittest.TestCase):
15+
def test_persistent_data_exists(self):
16+
self.assertTrue(os.path.isfile(gui.persistent_data.filename))
17+
18+
def test_read_write_persistent_data(self):
19+
data = gui.persistent_data
20+
keys = [
21+
"aggregator_hostname",
22+
"aggregator_port",
23+
"aggregator_secure",
24+
"uods",
25+
]
26+
for key in keys:
27+
data[key] = data[key]
28+
29+
30+
def engine_manager_factory(uods: List[str]) -> openpectus_engine_manager_gui.EngineManager:
31+
# Set up Engine Manager object with Null logging
32+
# handler and dict in memory instead of persistent
33+
# data. Override set_status_for_item with no-op.
34+
em = openpectus_engine_manager_gui.EngineManager(
35+
BufferingHandler(1000), # Capacity of 1000 records
36+
dict(
37+
aggregator_hostname="github.openpectus.org",
38+
aggregator_port=443,
39+
aggregator_secure=True,
40+
uods=[demo_uod.__file__],
41+
),
42+
)
43+
return em
44+
45+
46+
class TestEngineManager(unittest.TestCase):
47+
def test_start_stop_engine(self):
48+
# Test using demo UOD shipped with Open Pectus
49+
uods = [demo_uod.__file__]
50+
engine_item = dict(
51+
engine_name="Unittest",
52+
filename=demo_uod.__file__,
53+
)
54+
# Create list to hold engine status messages
55+
status_list = list()
56+
# Create Engine Manager
57+
em = engine_manager_factory(uods)
58+
em.set_status_for_item = lambda status, item: status_list.append(status)
59+
# Start engine and wait until fully started
60+
em.start_engine(engine_item)
61+
t0 = time.time()
62+
assert isinstance(em.log_handler, BufferingHandler)
63+
while True:
64+
buffer = em.log_handler.buffer
65+
if len(buffer) and buffer[-1].msg == "Started steady-state sending loop":
66+
break
67+
if time.time() - t0 >= 10:
68+
raise Exception("Engine manager was unable to start engine.")
69+
time.sleep(1)
70+
# Check engine is running properly
71+
self.assertIn(engine_item["engine_name"], em.loops)
72+
self.assertIn(engine_item["engine_name"], em.threads)
73+
loop = em.loops[engine_item["engine_name"]]
74+
thread = em.threads[engine_item["engine_name"]]
75+
self.assertTrue(loop.is_running())
76+
self.assertTrue(thread.is_alive())
77+
# Stop engine
78+
em.stop_engine(engine_item)
79+
t0 = time.time()
80+
while loop.is_running() or thread.is_alive():
81+
if time.time() - t0 >= 10:
82+
raise Exception("Engine manager was unable to stop engine.")
83+
time.sleep(1)
84+
85+
def test_validate_engine(self):
86+
# Test using demo UOD shipped with Open Pectus
87+
uods = [demo_uod.__file__]
88+
engine_item = dict(
89+
engine_name="Unittest",
90+
filename=demo_uod.__file__,
91+
)
92+
# Create list to hold engine status messages
93+
status_list = list()
94+
# Create Engine Manager
95+
em = engine_manager_factory(uods)
96+
em.set_status_for_item = lambda status, item: status_list.append(status)
97+
# Validate engine and wait until complete
98+
em.validate_engine(engine_item)
99+
self.assertIn(engine_item["engine_name"], em.threads)
100+
thread = em.threads[engine_item["engine_name"]]
101+
t0 = time.time()
102+
while thread.is_alive():
103+
if time.time() - t0 >= 30:
104+
raise Exception("Engine manager was unable to validate engine.")
105+
time.sleep(1)
106+
self.assertTrue(len(status_list))
107+
self.assertEqual(status_list[0], "Not running")
108+
109+
def test_validate_multiple_engines_simultaneously(self):
110+
# Test using demo UOD shipped with Open Pectus
111+
uods = [demo_uod.__file__]
112+
engine_item = dict(
113+
engine_name="Unittest",
114+
filename=demo_uod.__file__,
115+
)
116+
threads = []
117+
for i in range(5):
118+
# Create Engine Manager
119+
em = engine_manager_factory(uods)
120+
em.set_status_for_item = lambda status, item: None
121+
# Validate engine and wait until complete
122+
em.validate_engine(engine_item)
123+
threads.append(em.threads[engine_item["engine_name"]])
124+
t0 = time.time()
125+
while any([thread.is_alive() for thread in threads]):
126+
if time.time() - t0 >= 60:
127+
raise Exception("Engine manager was unable to validate engine.")
128+
time.sleep(1)
129+
130+
131+
if __name__ == "__main__":
132+
unittest.main()

0 commit comments

Comments
 (0)