30 Commits

Author SHA1 Message Date
stalbe_j
ca2caad46c new tests from before 2023-04-13 14:49:04 +02:00
stalbe_j
b13b91e55b Merge branch 'tests' of gitlab.psi.ch:augustin_s/grum into tests 2023-03-14 14:53:45 +01:00
stalbe_j
7780281e0c Merge branch 'master' of gitlab.psi.ch:augustin_s/grum into tests 2023-02-06 13:34:23 +01:00
76e3a04d98 added addEntrybox for max #entries 2023-02-04 18:54:36 +01:00
c620f17344 added addEntrybox 2023-02-04 18:53:53 +01:00
495a9a43bc added eviction, attached it to list changes signal 2023-02-04 18:52:49 +01:00
ab1d0d524e correctly set descending order (except for "sort by text") 2023-02-03 00:59:52 +01:00
cf4fbad5e7 update timestamps; added "Sort by timestamp" 2023-02-03 00:56:07 +01:00
5ea830bf2b attached timestamps object 2023-02-03 00:55:08 +01:00
87ae26247d attached timestamps object 2023-02-03 00:54:00 +01:00
23afec2aa4 fill xs and ys as argument 2023-02-03 00:53:19 +01:00
e4ada964e3 (trying to) disable sorting if item manually moved 2023-02-02 17:27:20 +01:00
9b50ade340 introduced "Sort by insertion order" (the default) / "Sorting disabled" keeps current order 2023-02-02 16:48:18 +01:00
26c338c54e added a comment 2023-02-02 14:59:51 +01:00
721f2a864a added docstrings for API methods 2023-02-02 14:59:42 +01:00
4c533489f6 added wildcard (*?) support 2023-02-02 13:28:52 +01:00
c7c17ef221 change sort key on the class (not the instances) 2023-02-02 11:59:05 +01:00
3ba74a1a1e added a tests run script 2023-02-01 16:17:58 +01:00
9e6896c30f fixed position of *args 2023-02-01 16:10:47 +01:00
829c9e2ded use different name inside function 2023-02-01 16:10:24 +01:00
9a66433a3e made LINE_COLORS tuple instead of list 2023-02-01 16:09:59 +01:00
9db8d5b87e cleanup 2023-02-01 15:31:31 +01:00
b380d840fc attach sorting options to menu 2023-02-01 15:26:38 +01:00
31cbe8ac33 added possibility to enabling/disabling sorting for the list 2023-02-01 15:26:17 +01:00
b02d66b95f added possibility for having a sort key 2023-02-01 15:25:37 +01:00
660e8208ca added offline mode (does not start the RPC server) 2023-01-29 16:32:41 +01:00
f491c71462 adjusted test 2023-01-29 14:19:34 +01:00
16c7b4f71c added maximize/restore to MDISubWindow, changed frame_on/frame_off from FramelessWindowHint to custom hint 2023-01-29 14:05:18 +01:00
aeaabd028f connect tooltip display with xy coords to plot mouse-over event 2023-01-27 18:50:34 +01:00
4e3d8b9da6 introduced MDISubPlotBase 2023-01-27 18:34:17 +01:00
19 changed files with 326 additions and 50 deletions

View File

@@ -25,13 +25,14 @@ def handle_clargs():
DESC = "grum - GUI for Remote Unified Monitoring" DESC = "grum - GUI for Remote Unified Monitoring"
parser = argparse.ArgumentParser(description=DESC, formatter_class=argparse.ArgumentDefaultsHelpFormatter) parser = argparse.ArgumentParser(description=DESC, formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("-H", "--host", default="localhost", help="Server host name") parser.add_argument("-H", "--host", default="localhost", help="RPC server host name")
parser.add_argument("-P", "--port", default=8000, type=int, help="Server port number") parser.add_argument("-P", "--port", default=8000, type=int, help="RPC server port number")
parser.add_argument("-o", "--offline", action="store_true", help="offline mode (do not run RPC server)")
parser.add_argument("-w", "--window-mode", default="multi", choices=MDIWindowMode.values(), type=unambiguous_window_mode, help="Set the initial window mode") parser.add_argument("-w", "--window-mode", default="multi", choices=MDIWindowMode.values(), type=unambiguous_window_mode, help="set the initial window mode")
parser.add_argument("-e", "--examples", dest="add_examples", action="store_true", help="Add example data") parser.add_argument("-e", "--examples", dest="add_examples", action="store_true", help="add example data")
parser.add_argument("--no-theme", action="store_true", help="Disable theming") parser.add_argument("--no-theme", action="store_true", help="disable theming")
return parser.parse_args().__dict__ return parser.parse_args().__dict__

View File

@@ -1,4 +1,7 @@
from PyQt5.QtWidgets import QWidget, QLineEdit, QVBoxLayout import fnmatch
import re
from PyQt5.QtWidgets import QWidget, QVBoxLayout
from .dictlistwidget import DictListWidget from .dictlistwidget import DictListWidget
from .searchbox import SearchBox from .searchbox import SearchBox
@@ -31,11 +34,26 @@ class DictList(QWidget):
def hide_not_matching(self, pattern): def hide_not_matching(self, pattern):
pattern = pattern.casefold() g = Globber(pattern)
for name, itm in self.lst.items.items(): for name, itm in self.lst.items.items():
name = name.casefold() state = g.match(name)
state = (pattern not in name) itm.setHidden(not state)
itm.setHidden(state)
class Globber:
def __init__(self, pattern):
pattern = pattern.casefold()
pattern = "*" + pattern + "*"
regex = fnmatch.translate(pattern)
self.pattern = re.compile(regex)
def match(self, string):
string = string.casefold()
matches = self.pattern.match(string)
state = bool(matches)
return state

View File

@@ -3,6 +3,7 @@ from PyQt5.QtWidgets import QListWidgetItem
from PyQt5.QtGui import QIcon, QPixmap, QPainter from PyQt5.QtGui import QIcon, QPixmap, QPainter
from ..theme import DOT_PEN, DOT_BRUSH, DOT_SIZE from ..theme import DOT_PEN, DOT_BRUSH, DOT_SIZE
from .timestamps import Timestamps
class DictListItem(QListWidgetItem): class DictListItem(QListWidgetItem):
@@ -11,9 +12,25 @@ class DictListItem(QListWidgetItem):
super().__init__(key, *args, **kwargs) super().__init__(key, *args, **kwargs)
self.key = key self.key = key
self.value = value self.value = value
self.timestamps = Timestamps()
self.set_alarm(False) self.set_alarm(False)
_sort_key = None
@classmethod # need to update the class so that all instances are always consistent
def set_sort_key(cls, sk):
cls._sort_key = staticmethod(sk) # need to attach as staticmethod, otherwise self is inserted as first argument
def __lt__(self, other):
assert self._sort_key == other._sort_key
sk = self._sort_key
if sk:
return sk(self) < sk(other)
else:
return super().__lt__(other)
def set_alarm(self, state): def set_alarm(self, state):
self.set_bold(state) self.set_bold(state)
self.set_dot(state) self.set_dot(state)

View File

@@ -14,8 +14,12 @@ class DictListWidget(QListWidget):
self.setSelectionMode(QListWidget.ExtendedSelection) self.setSelectionMode(QListWidget.ExtendedSelection)
self.items = {} self.items = {}
self._add_menu() self._add_menu()
shortcut(self, "Del", self.delete_selected, context=Qt.WidgetShortcut) shortcut(self, "Del", self.delete_selected, context=Qt.WidgetShortcut)
self.nkeep = None
self.model().rowsInserted.connect(self.on_evict)
def _add_menu(self): def _add_menu(self):
self.menu = menu = RClickMenu(self) self.menu = menu = RClickMenu(self)
@@ -72,4 +76,69 @@ class DictListWidget(QListWidget):
i.set_alarm(state) i.set_alarm(state)
def set_sort_key(self, sk, reverse=True):
DictListItem.set_sort_key(sk)
if sk is None:
self.setSortingEnabled(False)
else:
self.setSortingEnabled(True)
order = Qt.DescendingOrder if reverse else Qt.AscendingOrder
self.sortItems(order)
def enable_sort_by_insertion_order(self):
# map order in dict to indices
mapping = enumerate(self.items.values())
mapping = {item.text(): index for index, item in mapping}
def unsort(x):
return mapping[x.text()]
self.set_sort_key(unsort)
self.disable_sorting()
def enable_sort_by_text(self):
self.set_sort_key(lambda x: x.text(), reverse=False)
def enable_sort_by_timestamp(self):
# fall back to name for identical timestamps
self.set_sort_key(lambda x: (x.timestamps.max(), x.text()))
def disable_sorting(self):
self.set_sort_key(None)
def set_nkeep(self, n):
self.nkeep = n
self.on_evict()
def on_evict(self):
if self.nkeep:
self.evict(self.nkeep)
def evict(self, nkeep):
items = self.items.values()
# map order in dict to indices
mapping = enumerate(items)
mapping = {item.text(): index for index, item in mapping}
def sk(x):
# fall back to insertion order for identical timestamps
return (x.timestamps.max(), mapping[x.text()])
items = sorted(items, key=sk, reverse=True)
items_to_evict = items[nkeep:]
for i in items_to_evict:
self.items.pop(i.key)
self.deleteItem(i)

View File

@@ -47,7 +47,7 @@ class SearchBox(QWidget):
class SquareButton(QPushButton): class SquareButton(QPushButton):
def resizeEvent(self, e): def resizeEvent(self, _event):
height = self.height() height = self.height()
self.setMinimumWidth(height) self.setMinimumWidth(height)

View File

@@ -20,15 +20,15 @@ for i in range(20):
exampledata = {} exampledata = {}
for k, v in exampledata_raw.items(): for name, (xs, ys) in exampledata_raw.items():
pd = PlotDescription( exampledata[name] = PlotDescription(
k, name,
# title=k, # title=name,
xlabel="x", xlabel="x",
ylabel="y" ylabel="y",
xs=xs,
ys=ys
) )
pd.xs, pd.ys = v
exampledata[k] = pd

View File

@@ -31,7 +31,7 @@ def read_dict(fn):
data = node[()] data = node[()]
if isinstance(data, bytes): if isinstance(data, bytes):
data = data.decode("utf-8") data = data.decode("utf-8")
res[node.name] = data res[name] = data
with h5py.File(fn, "r") as f: with h5py.File(fn, "r") as f:
f.visititems(visit) f.visititems(visit)
@@ -44,7 +44,7 @@ def unflatten_dict(d, sep="/"):
for k, v in d.items(): for k, v in d.items():
current = res current = res
levels = k.split(sep) levels = k.split(sep)
for l in levels[1:-1]: for l in levels[:-1]:
if l not in current: if l not in current:
current[l] = {} current[l] = {}
current = current[l] current = current[l]

View File

@@ -18,8 +18,12 @@ class MainWindow(QMainWindow):
sig_make_new_plot = pyqtSignal(str, PlotDescription) sig_make_new_plot = pyqtSignal(str, PlotDescription)
def __init__(self, *args, title="grum", host="localhost", port=8000, add_examples=False, window_mode=MDIWindowMode.MULTI, **kwargs): def __init__(self, *args, title="grum", host="localhost", port=8000, offline=False, add_examples=False, window_mode=MDIWindowMode.MULTI, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
if offline:
title = f"{title} (offline)"
self.setWindowTitle(title) self.setWindowTitle(title)
self.setWindowIcon(assets.icon()) self.setWindowIcon(assets.icon())
@@ -39,6 +43,19 @@ class MainWindow(QMainWindow):
lst_menu.addSeparator() lst_menu.addSeparator()
lst_menu.addAction("Mark selected as seen", self.on_mark_selected_as_seen) lst_menu.addAction("Mark selected as seen", self.on_mark_selected_as_seen)
lst_menu.addAction("Mark selected as not seen", self.on_mark_selected_as_not_seen) lst_menu.addAction("Mark selected as not seen", self.on_mark_selected_as_not_seen)
lst_menu.addSeparator()
sort_group = lst_menu.addGroup()
sort_group.addCheckbox("Sort by insertion order", triggered=self.on_sort_by_insertion_order, state=True)
sort_group.addCheckbox("Sort by name", triggered=self.on_sort_by_name)
sort_group.addCheckbox("Sort by timestamp", triggered=self.on_sort_by_timestamp)
sort_group.addCheckbox("Sorting disabled", triggered=self.on_sorting_disabled)
#TODO: clean up
def on_item_about_to_be_moved():
sort_group.checkboxes["Sorting disabled"].setChecked(True)
self.on_sorting_disabled()
self.lst.model().rowsAboutToBeMoved.connect(on_item_about_to_be_moved)
shortcut(self, "Ctrl+P", self.on_plot_selected) shortcut(self, "Ctrl+P", self.on_plot_selected)
@@ -52,6 +69,8 @@ class MainWindow(QMainWindow):
self.menu_settings = menu = BarMenu(bar, "&Settings") self.menu_settings = menu = BarMenu(bar, "&Settings")
menu.addCheckbox("Open new plots", state=True) menu.addCheckbox("Open new plots", state=True)
menu.addSeparator()
menu.addEntrybox("Limit number of entries", placeholder="Maximum number of entries", triggered=lst.set_nkeep)
self.mdi = mdi = MDIArea(bar, window_mode=window_mode) self.mdi = mdi = MDIArea(bar, window_mode=window_mode)
@@ -62,10 +81,11 @@ class MainWindow(QMainWindow):
self.setCentralWidget(splitter) self.setCentralWidget(splitter)
self.rst = rst = RPCServerThread(host, port, doc_title_suffix=title) if not offline:
rst.start() self.rst = rst = RPCServerThread(host, port, doc_title_suffix=title)
rst.server.register_function(self.new_plot) rst.start()
rst.server.register_function(self.append_data) rst.server.register_function(self.new_plot)
rst.server.register_function(self.append_data)
self.sig_make_new_plot.connect(self.on_make_new_plot) self.sig_make_new_plot.connect(self.on_make_new_plot)
@@ -78,12 +98,21 @@ class MainWindow(QMainWindow):
# Remote API calls # Remote API calls
def new_plot(self, name, cfg): def new_plot(self, name, cfg):
"""
Create a new plot <name> using the configuration dict <cfg>.
The configuration is forwarded to the constructor of PlotDescription.
Allowed keys are: title, xlabel, ylabel, xs, ys.
"""
desc = self.add_new_desc_to_list(name, cfg) desc = self.add_new_desc_to_list(name, cfg)
if self.menu_settings.checkboxes["Open new plots"].isChecked(): if self.menu_settings.checkboxes["Open new plots"].isChecked():
if not self.mdi.findSubWindow(name): if not self.mdi.findSubWindow(name):
self.sig_make_new_plot.emit(name, desc) self.sig_make_new_plot.emit(name, desc)
def append_data(self, name, point): def append_data(self, name, point):
"""
Append a new data point <point> to the (existing) plot <name>.
The point is forwarded to the append method of PlotDescription.
"""
item = self.lst.get(name) item = self.lst.get(name)
desc = item.value desc = item.value
desc.append(point) desc.append(point)
@@ -93,6 +122,7 @@ class MainWindow(QMainWindow):
plot = sub.plots[name] plot = sub.plots[name]
plot.setData(*desc.data) plot.setData(*desc.data)
alarm = False alarm = False
item.timestamps.modification.update()
item.set_alarm(alarm) item.set_alarm(alarm)
@@ -114,6 +144,7 @@ class MainWindow(QMainWindow):
else: else:
self.plot_multiple_items(selected) self.plot_multiple_items(selected)
def on_mark_selected_as_seen(self): def on_mark_selected_as_seen(self):
self.lst.set_alarm_for_selected(False) self.lst.set_alarm_for_selected(False)
@@ -121,6 +152,19 @@ class MainWindow(QMainWindow):
self.lst.set_alarm_for_selected(True) self.lst.set_alarm_for_selected(True)
def on_sort_by_insertion_order(self):
self.lst.enable_sort_by_insertion_order()
def on_sort_by_name(self):
self.lst.enable_sort_by_text()
def on_sort_by_timestamp(self):
self.lst.enable_sort_by_timestamp()
def on_sorting_disabled(self):
self.lst.disable_sorting()
def on_file_open(self): def on_file_open(self):
fns = open_h5_files_dialog(self) fns = open_h5_files_dialog(self)
if not fns: if not fns:
@@ -153,12 +197,14 @@ class MainWindow(QMainWindow):
return desc return desc
def plot_single_item(self, item): def plot_single_item(self, item):
item.timestamps.access.update()
item.set_alarm(False) item.set_alarm(False)
name, desc = item.key, item.value name, desc = item.key, item.value
self.activate_or_make_subwin(MDISubPlot, name, desc) self.activate_or_make_subwin(MDISubPlot, name, desc)
def plot_multiple_items(self, items): def plot_multiple_items(self, items):
for i in items: for i in items:
i.timestamps.access.update()
i.set_alarm(False) i.set_alarm(False)
descs = {i.key: i.value for i in items} descs = {i.key: i.value for i in items}
names = descs.keys() names = descs.keys()

View File

@@ -20,7 +20,7 @@ class MDIWindowMode(str, enum.Enum):
class MDIArea(QMdiArea): class MDIArea(QMdiArea):
def __init__(self, bar, window_mode=MDIWindowMode.MULTI, *args, **kwargs): def __init__(self, bar, *args, window_mode=MDIWindowMode.MULTI, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.logo = assets.logo() self.logo = assets.logo()
self.setTabsClosable(True) self.setTabsClosable(True)
@@ -67,6 +67,7 @@ class MDIArea(QMdiArea):
self.menu.checkboxes["Multiple windows"].setChecked(True) self.menu.checkboxes["Multiple windows"].setChecked(True)
self.enable_subwindow_view() self.enable_subwindow_view()
for sub in self.subWindowList(): for sub in self.subWindowList():
sub.restore()
sub.frame_on() sub.frame_on()
def enable_single_window_mode(self): def enable_single_window_mode(self):
@@ -75,7 +76,7 @@ class MDIArea(QMdiArea):
self.closeInactiveSubWindows() self.closeInactiveSubWindows()
active = self.activeSubWindow() active = self.activeSubWindow()
if active: if active:
active.showMaximized() active.maximize()
active.frame_off() active.frame_off()
def enable_tabbed_mode(self): def enable_tabbed_mode(self):
@@ -104,7 +105,7 @@ class MDIArea(QMdiArea):
def add_single(self, sub): def add_single(self, sub):
self.closeAllSubWindows() self.closeAllSubWindows()
self.addSubWindow(sub) self.addSubWindow(sub)
sub.showMaximized() sub.maximize()
sub.frame_off() sub.frame_off()

View File

@@ -3,30 +3,48 @@ from .mdisubwin import MDISubWindow
from ..theme import pg_plot_style, pg_plot_style_cycler, pg_legend_style from ..theme import pg_plot_style, pg_plot_style_cycler, pg_legend_style
class MDISubPlot(MDISubWindow): class MDISubPlotBase(MDISubWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.pw = pw = pg.PlotWidget()
self.setWidget(pw)
# connect to plot mouse-over event
pw.scene().sigMouseMoved.connect(self.on_hover)
def on_hover(self, event):
coord = self.pw.plotItem.vb.mapSceneToView(event)
x = coord.x()
y = coord.y()
x = round(x, 3)
y = round(y, 3)
self.setToolTip(f"x = {x}\ny = {y}")
class MDISubPlot(MDISubPlotBase):
def __init__(self, name, desc, *args, **kwargs): def __init__(self, name, desc, *args, **kwargs):
super().__init__(name, *args, **kwargs) super().__init__(name, *args, **kwargs)
pw = pg.PlotWidget()
self.setWidget(pw)
style = pg_plot_style() style = pg_plot_style()
plot = desc.make_plot(pw, style) plot = desc.make_plot(self.pw, style)
self.plots = {name: plot} self.plots = {name: plot}
class MDISubMultiPlot(MDISubWindow): class MDISubMultiPlot(MDISubPlotBase):
def __init__(self, name, descs, *args, **kwargs): def __init__(self, name, descs, *args, **kwargs):
super().__init__(name, *args, **kwargs) super().__init__(name, *args, **kwargs)
pw = pg.PlotWidget()
self.setWidget(pw)
ls = pg_legend_style() ls = pg_legend_style()
psc = pg_plot_style_cycler() psc = pg_plot_style_cycler()
pw = self.pw
pw.addLegend(**ls) pw.addLegend(**ls)
self.plots = {name: desc.make_plot(pw, style) for (name, desc), style in zip(descs.items(), psc)} self.plots = {name: desc.make_plot(pw, style) for (name, desc), style in zip(descs.items(), psc)}

View File

@@ -14,12 +14,27 @@ class MDISubWindow(QMdiSubWindow):
# without this, the SubWindow is not removed from the subWindowList # without this, the SubWindow is not removed from the subWindowList
self.setAttribute(Qt.WA_DeleteOnClose) self.setAttribute(Qt.WA_DeleteOnClose)
self._previous_state = None
def maximize(self):
self._previous_state = state = self.windowState()
self.setWindowState(state | Qt.WindowMaximized)
def restore(self):
if self._previous_state:
self.setWindowState(self._previous_state)
def frame_on(self): def frame_on(self):
self.setWindowFlag(Qt.FramelessWindowHint, False) self.hide()
self.setWindowFlags(Qt.SubWindow)
self.show()
def frame_off(self): def frame_off(self):
self.setWindowFlag(Qt.FramelessWindowHint, True) self.hide()
self.setWindowFlags(Qt.CustomizeWindowHint | Qt.WindowTitleHint)
self.show()

View File

@@ -1,5 +1,5 @@
from PyQt5.QtGui import QKeySequence from PyQt5.QtGui import QKeySequence, QIntValidator
from PyQt5.QtWidgets import QAction, QActionGroup from PyQt5.QtWidgets import QAction, QActionGroup, QWidgetAction, QLineEdit
class MenuBase: class MenuBase:
@@ -31,6 +31,48 @@ class MenuBase:
self.checkboxes[name] = action self.checkboxes[name] = action
return action return action
def addEntrybox(self, name, placeholder=None, state=False, triggered=None):
cb = self.addCheckbox(name, state=state)
edit = QLineEdit(self.qmenu)
edit.setValidator(QIntValidator()) #TODO: make optional
edit.setEnabled(state)
if placeholder:
edit.setPlaceholderText(placeholder)
edit.setContentsMargins(8, 5, 8, 5) # mimic margins of other actions TODO: this probably depends on the theme
@cb.toggled.connect
def propagate_state_and_keep_menu_open_and_focus_edit(checked):
edit.setEnabled(checked)
if checked:
self.qmenu.show()
self.qmenu.setActiveAction(cb)
edit.setFocus()
@edit.returnPressed.connect
def close_menu():
self.qmenu.close()
if triggered:
@cb.triggered.connect
def confirm_disabled(checked):
if not checked:
triggered(None)
if triggered:
@edit.returnPressed.connect
def confirm_value():
value = edit.text()
value = int(value)
triggered(value)
action = QWidgetAction(self.qmenu)
action.setDefaultWidget(edit)
self.qmenu.addAction(action)
return action
def addSeparator(self): def addSeparator(self):
self.qmenu.addSeparator() self.qmenu.addSeparator()

View File

@@ -4,7 +4,7 @@ from inspect import getdoc, signature
class RPCServer(xrs.DocXMLRPCServer): class RPCServer(xrs.DocXMLRPCServer):
def __init__(self, host, port, doc_title_suffix="", *args, **kwargs): def __init__(self, host, port, *args, doc_title_suffix="", **kwargs):
addr = (host, port) addr = (host, port)
kwargs.setdefault("allow_none", True) kwargs.setdefault("allow_none", True)
super().__init__(addr, *args, **kwargs) super().__init__(addr, *args, **kwargs)

View File

@@ -4,9 +4,9 @@ from PyQt5.QtWidgets import QShortcut
def shortcut(parent, key_sequence, triggered, **kwargs): def shortcut(parent, key_sequence, triggered, **kwargs):
key_sequence = QKeySequence(key_sequence) key_sequence = QKeySequence(key_sequence)
shortcut = QShortcut(key_sequence, parent, **kwargs) sc = QShortcut(key_sequence, parent, **kwargs)
shortcut.activated.connect(triggered) sc.activated.connect(triggered)
return shortcut return sc

View File

@@ -27,7 +27,7 @@ DOT_PEN = GREY5
DOT_BRUSH = HIGHLIGHT DOT_BRUSH = HIGHLIGHT
DOT_SIZE = 8 DOT_SIZE = 8
LINE_COLORS = [ LINE_COLORS = (
C.BLUE, C.BLUE,
C.BASE_GREEN, C.BASE_GREEN,
C.RED, C.RED,
@@ -38,7 +38,7 @@ LINE_COLORS = [
C.COOL3, C.COOL3,
C.BROWN1, C.BROWN1,
C.BASE_ORANGE, C.BASE_ORANGE,
] )

12
tests/run.sh Executable file
View File

@@ -0,0 +1,12 @@
#!/bin/bash
cd $(dirname $0)
cd ..
export QT_QPA_PLATFORM=offscreen
coverage run --source=./grum/ -m pytest ./tests/
echo
coverage report

View File

@@ -0,0 +1,27 @@
from grum.dictlist.dictlistwidget import DictListWidget
from grum.menus.rclickmenu import RClickMenu
class DictListWidgetMock(DictListWidget):
def __init__(self) -> None:
self.items = {}
self.nkeep = None
def get_DictListWidgetMock():
return DictListWidgetMock()
# def test_defaults():
# dlw = get_DictListWidgetMock()
# assert dlw.items == {}
# assert dlw.nkeep == None
# def test_add_menu():
# dlw = get_DictListWidgetMock()
# dlw._add_menu()
# assert dlw.menu == RClickMenu(dlw)

View File

@@ -21,7 +21,6 @@ from grum.rpc import RPCServerThread
class TestMainWin: class TestMainWin:
def setup_method(self): def setup_method(self):
print("setup")
self.app = QApplication(sys.argv) self.app = QApplication(sys.argv)
theme.apply(self.app) theme.apply(self.app)
# ctrl_c.setup(self.app) # ctrl_c.setup(self.app)
@@ -30,10 +29,8 @@ class TestMainWin:
def teardown_method(self): def teardown_method(self):
print("teardown")
self.mw.rst.wait_for_stop() self.mw.rst.wait_for_stop()
self.app.quit() self.app.quit()
print("app quit called")
del self.mw del self.mw
del self.app del self.app
@@ -168,6 +165,19 @@ class TestMainWin:
mw.plot_multiple_items.assert_called_once_with([sine_item, cosine_item]) mw.plot_multiple_items.assert_called_once_with([sine_item, cosine_item])
def test_on_sort_by_name(self):
mw = self.mw
mw.lst = DictList()
mw.new_plot("bb", {})
mw.new_plot("dd", {})
mw.new_plot("aa", {})
mw.new_plot("cc", {})
assert mw.lst.lst.items == 111
assert [key for key in mw.lst.lst.items.keys()] == ['bb', 'dd', 'aa', 'cc']
mw.on_sort_by_name()
assert mw.lst.lst.items == 111
def test_plot_single_item(self): def test_plot_single_item(self):
mw = self.mw mw = self.mw
sine_item = mw.lst.lst.get("sine") sine_item = mw.lst.lst.get("sine")

View File

@@ -143,14 +143,14 @@ class TestMDIArea:
mdi.closeAllSubWindows = mock.MagicMock() mdi.closeAllSubWindows = mock.MagicMock()
sine_item = self.mw.lst.lst.get("sine") sine_item = self.mw.lst.lst.get("sine")
sub = MDISubPlot("sine", sine_item.value) sub = MDISubPlot("sine", sine_item.value)
sub.showMaximized = mock.MagicMock() sub.maximize = mock.MagicMock()
sub.frame_off = mock.MagicMock() sub.frame_off = mock.MagicMock()
mdi.add_single(sub) mdi.add_single(sub)
mdi.closeAllSubWindows.assert_called_once() mdi.closeAllSubWindows.assert_called_once()
mdi.addSubWindow.assert_called_once_with(sub) mdi.addSubWindow.assert_called_once_with(sub)
sub.showMaximized.assert_called_once() sub.maximize.assert_called_once()
sub.frame_off.assert_called_once() sub.frame_off.assert_called_once()