diff --git a/src/firefly/run_browser.py b/src/firefly/run_browser.py index 6c515ce8..e3377002 100644 --- a/src/firefly/run_browser.py +++ b/src/firefly/run_browser.py @@ -1,5 +1,7 @@ import asyncio import logging +from collections import Counter +from contextlib import contextmanager from functools import wraps from itertools import count from typing import Mapping, Sequence @@ -8,6 +10,7 @@ import qtawesome as qta import yaml from matplotlib.colors import TABLEAU_COLORS +from pandas.api.types import is_numeric_dtype from pyqtgraph import GraphicsLayoutWidget, ImageView, PlotItem, PlotWidget from qasync import asyncSlot from qtpy.QtCore import Qt, Signal @@ -110,6 +113,7 @@ def plot_runs(self, runs: Mapping, xsignal: str): ysignals = sorted(list(dict.fromkeys(ysignals))) # Plot the runs self.clear() + self._multiplot_items = {} for label, data in runs.items(): # Figure out which signals to plot try: @@ -120,7 +124,8 @@ def plot_runs(self, runs: Mapping, xsignal: str): # Plot each y signal on a separate plot for ysignal, plot_item in zip(ysignals, self.multiplot_items()): try: - plot_item.plot(xdata, data[ysignal]) + if is_numeric_dtype(data[ysignal]): + plot_item.plot(xdata, data[ysignal]) except KeyError: log.warning(f"No signal {ysignal} in data.") else: @@ -237,13 +242,19 @@ class RunBrowserDisplay(display.FireflyDisplay): selected_runs: list _running_db_tasks: Mapping + # Counter for keeping track of UI hints for long DB hits + _busy_hinters: Counter + def __init__(self, root_node=None, args=None, macros=None, **kwargs): super().__init__(args=args, macros=macros, **kwargs) self.selected_runs = [] self._running_db_tasks = {} + self._busy_hinters = Counter() self.db = DatabaseWorker(catalog=root_node) # Load the list of all runs for the selection widget - self.db_task(self.load_runs()) + self.db_task(self.load_runs(), name="init_load_runs") + # Load the list of filters' field values into the comboboxes + self.db_task(self.update_combobox_items(), name="update_combobox_items") def db_task(self, coro, name="default task"): """Executes a co-routine as a database task. Existing database @@ -268,53 +279,23 @@ async def reload_runs(self): @cancellable async def load_runs(self): """Get the list of available runs based on filters.""" - runs = await self.db_task( - self.db.load_all_runs(self.filters()), - name="load all runs", - ) - # Update the table view data model - self.runs_model.clear() - self.runs_model.setHorizontalHeaderLabels(self._run_col_names) - for run in runs: - items = [QStandardItem(val) for val in run.values()] - self.ui.runs_model.appendRow(items) - # Adjust the layout of the data table - sort_col = self._run_col_names.index("Datetime") - self.ui.run_tableview.sortByColumn(sort_col, Qt.DescendingOrder) - self.ui.run_tableview.resizeColumnsToContents() - # Let slots know that the model data have changed - self.runs_total_label.setText(str(self.ui.runs_model.rowCount())) - - # # def start_run_client(self, root_node): - # # """Set up the database client in a separate thread.""" - # # # Create the thread and worker - # # thread = QThread(parent=self) - # # self._thread = thread - # # worker = DatabaseWorker(root_node=root_node) - # # self._db_worker = worker - # # worker.moveToThread(thread) - # # # Set up filters - # # worker.new_message.connect(self.show_message) - # # self.filters_changed.connect(worker.set_filters) - # # # Connect signals/slots - # # thread.started.connect(worker.load_all_runs) - # # worker.all_runs_changed.connect(self.set_runs_model_items) - # # worker.selected_runs_changed.connect(self.update_metadata) - # # worker.selected_runs_changed.connect(self.update_1d_signals) - # # worker.selected_runs_changed.connect(self.update_2d_signals) - # # worker.selected_runs_changed.connect(self.update_1d_plot) - # # worker.selected_runs_changed.connect(self.update_2d_plot) - # # worker.selected_runs_changed.connect(self.update_multi_plot) - # # worker.db_op_started.connect(self.disable_run_widgets) - # # worker.db_op_ended.connect(self.enable_run_widgets) - # # # Make sure filters are current - # # self.update_filters() - # # # Start the thread - # # thread.start() - # # # Get distinct fields so we can populate the comboboxes - # # self.load_distinct_fields.connect(worker.load_distinct_fields) - # # worker.distinct_fields_changed.connect(self.update_combobox_items) - # # self.load_distinct_fields.emit() + with self.busy_hints(run_widgets=True, run_table=True, filter_widgets=False): + runs = await self.db_task( + self.db.load_all_runs(self.filters()), + name="load all runs", + ) + # Update the table view data model + self.runs_model.clear() + self.runs_model.setHorizontalHeaderLabels(self._run_col_names) + for run in runs: + items = [QStandardItem(val) for val in run.values()] + self.ui.runs_model.appendRow(items) + # Adjust the layout of the data table + sort_col = self._run_col_names.index("Datetime") + self.ui.run_tableview.sortByColumn(sort_col, Qt.DescendingOrder) + self.ui.run_tableview.resizeColumnsToContents() + # Let slots know that the model data have changed + self.runs_total_label.setText(str(self.ui.runs_model.rowCount())) def clear_filters(self): self.ui.filter_proposal_combobox.setCurrentText("") @@ -328,39 +309,42 @@ def clear_filters(self): self.ui.filter_edge_combobox.setCurrentText("") self.ui.filter_user_combobox.setCurrentText("") - # def update_combobox_items(self, fields): - # for field_name, cb in [ - # ("proposal_users", self.ui.filter_proposal_combobox), - # ("proposal_id", self.ui.filter_user_combobox), - # ("esaf_id", self.ui.filter_esaf_combobox), - # ("sample_name", self.ui.filter_sample_combobox), - # ("plan_name", self.ui.filter_plan_combobox), - # ("edge", self.ui.filter_edge_combobox), - # ]: - # if field_name in fields.keys(): - # old_text = cb.currentText() - # cb.clear() - # cb.addItems(fields[field_name]) - # cb.setCurrentText(old_text) + async def update_combobox_items(self): + """""" + with self.busy_hints(run_table=False, run_widgets=False, filter_widgets=True): + fields = await self.db.load_distinct_fields() + for field_name, cb in [ + ("proposal_users", self.ui.filter_proposal_combobox), + ("proposal_id", self.ui.filter_user_combobox), + ("esaf_id", self.ui.filter_esaf_combobox), + ("sample_name", self.ui.filter_sample_combobox), + ("plan_name", self.ui.filter_plan_combobox), + ("edge", self.ui.filter_edge_combobox), + ]: + if field_name in fields.keys(): + old_text = cb.currentText() + cb.clear() + cb.addItems(fields[field_name]) + cb.setCurrentText(old_text) @asyncSlot() @cancellable async def sleep_slot(self): - print("Sleeping") await self.db_task(self.print_sleep()) async def print_sleep(self): - label = self.ui.sleep_label - label.setText(f"3...") - await asyncio.sleep(1) - old_text = label.text() - label.setText(f"{old_text}2...") - await asyncio.sleep(1) - old_text = label.text() - label.setText(f"{old_text}1...") - await asyncio.sleep(1) - old_text = label.text() - label.setText(f"{old_text}done!") + with self.busy_hints(run_widgets=True, run_table=True, filter_widgets=True): + label = self.ui.sleep_label + label.setText(f"3...") + await asyncio.sleep(1) + old_text = label.text() + label.setText(f"{old_text}2...") + await asyncio.sleep(1) + old_text = label.text() + label.setText(f"{old_text}1...") + await asyncio.sleep(1) + old_text = label.text() + label.setText(f"{old_text}done!") def customize_ui(self): self.load_models() @@ -382,6 +366,10 @@ def customize_ui(self): self.ui.invert_checkbox.stateChanged.connect(self.update_1d_plot) self.ui.gradient_checkbox.stateChanged.connect(self.update_1d_plot) self.ui.plot_1d_hints_checkbox.stateChanged.connect(self.update_1d_signals) + self.ui.plot_multi_hints_checkbox.stateChanged.connect( + self.update_multi_signals + ) + self.ui.plot_multi_hints_checkbox.stateChanged.connect(self.update_multi_plot) # Respond to changes in displaying the 2d plot self.ui.signal_value_combobox.currentTextChanged.connect(self.update_2d_plot) self.ui.logarithm_checkbox_2d.stateChanged.connect(self.update_2d_plot) @@ -398,29 +386,90 @@ def customize_ui(self): self.ui.hover_coords_label.setText ) - # def disable_run_widgets(self): - # self.show_message("Loading...") - # widgets = [ - # self.ui.run_tableview, - # self.ui.refresh_runs_button, - # self.ui.detail_tabwidget, - # self.ui.runs_total_layout, - # self.ui.filters_widget, - # ] - # for widget in widgets: - # widget.setEnabled(False) - # self.disabled_widgets = widgets - # self.setCursor(Qt.WaitCursor) - - # def enable_run_widgets(self, exceptions=[]): - # if any(exceptions): - # self.show_message(exceptions[0]) - # else: - # self.show_message("Done", 5000) - # # Re-enable the widgets - # for widget in self.disabled_widgets: - # widget.setEnabled(True) - # self.setCursor(Qt.ArrowCursor) + def update_busy_hints(self): + """Enable/disable UI elements based on the active hinters.""" + # Widgets for showing plots for runs + if self._busy_hinters["run_widgets"] > 0: + self.ui.detail_tabwidget.setEnabled(False) + else: + # Re-enable the run widgets + self.ui.detail_tabwidget.setEnabled(True) + # Widgets for selecting which runs to show + if self._busy_hinters["run_table"] > 0: + self.ui.run_tableview.setEnabled(False) + else: + # Re-enable the run widgets + self.ui.run_tableview.setEnabled(True) + # Widgets for filtering runs + if self._busy_hinters["filters_widget"] > 0: + self.ui.filters_widget.setEnabled(False) + else: + self.ui.filters_widget.setEnabled(True) + # Update status message in message bars + if len(list(self._busy_hinters.elements())) > 0: + self.show_message("Loading…") + else: + self.show_message("Done.", 5000) + + @contextmanager + def busy_hints(self, run_widgets=True, run_table=True, filter_widgets=True): + """A context manager that displays UI hints when slow operations happen. + + Arguments can be used to control which widgets are modified. + + Usage: + + .. code-block:: python + + with self.busy_hints(): + self.db_task(self.slow_operation) + + Parameters + ========== + run_widgets + Disable the widgets for viewing individual runs. + run_table + Disable the table for selecting runs to view. + filter_widgets + Disable the filter comboboxes, etc. + + """ + # Update the counters for keeping track of concurrent contexts + hinters = { + "run_widgets": run_widgets, + "run_table": run_table, + "filters_widget": filter_widgets, + } + hinters = [name for name, include in hinters.items() if include] + self._busy_hinters.update(hinters) + # Update the UI (e.g. disable widgets) + self.update_busy_hints() + # Run the innner context code + try: + yield + finally: + # Re-enable widgets if appropriate + self._busy_hinters.subtract(hinters) + self.update_busy_hints() + + @asyncSlot() + @cancellable + async def update_multi_signals(self, *args): + """Retrieve a new list of signals for multi plot and update UI.""" + combobox = self.ui.multi_signal_x_combobox + # Store old value for restoring later + old_value = combobox.currentText() + # Determine valid list of columns to choose from + use_hints = self.ui.plot_multi_hints_checkbox.isChecked() + signals_task = self.db_task( + self.db.signal_names(hinted_only=use_hints), "multi signals" + ) + xcols, ycols = await signals_task + # Update the comboboxes with new signals + combobox.clear() + combobox.addItems(xcols) + # Restore previous value + combobox.setCurrentText(old_value) @asyncSlot() @cancellable @@ -440,9 +489,8 @@ async def update_1d_signals(self, *args): xcols, ycols = await signals_task self.multi_y_signals = ycols # Update the comboboxes with new signals - for cb in [self.ui.multi_signal_x_combobox, self.ui.signal_x_combobox]: - cb.clear() - cb.addItems(xcols) + self.ui.signal_x_combobox.clear() + self.ui.signal_x_combobox.addItems(xcols) for cb in [ self.ui.signal_y_combobox, self.ui.signal_r_combobox, @@ -470,53 +518,13 @@ async def update_2d_signals(self, *args): # Restore previous selection val_cb.setCurrentText(old_value) - # def calculate_ydata( - # self, - # x_data, - # y_data, - # r_data, - # x_signal, - # y_signal, - # r_signal, - # use_reference=False, - # use_log=False, - # use_invert=False, - # use_grad=False, - # ): - # """Take raw y and reference data and calculate a new y_data signal.""" - # # Make sure we have numpy arrays - # x = np.asarray(x_data) - # y = np.asarray(y_data) - # r = np.asarray(r_data) - # # Apply transformations - # y_string = f"[{y_signal}]" - # try: - # if use_reference: - # y = y / r - # y_string = f"{y_string}/[{r_signal}]" - # if use_log: - # y = np.log(y) - # y_string = f"ln({y_string})" - # if use_invert: - # y *= -1 - # y_string = f"-{y_string}" - # if use_grad: - # y = np.gradient(y, x) - # y_string = f"d({y_string})/d[{r_signal}]" - # except TypeError as exc: - # msg = f"Could not calculate transformation: {exc}" - # log.warning(msg) - # raise - # raise exceptions.InvalidTransformation(msg) - # return y, y_string - @asyncSlot() @cancellable async def update_multi_plot(self, *args): x_signal = self.ui.multi_signal_x_combobox.currentText() if x_signal == "": return - use_hints = self.ui.plot_1d_hints_checkbox.isChecked() + use_hints = self.ui.plot_multi_hints_checkbox.isChecked() runs = await self.db_task( self.db.all_signals(hinted_only=use_hints), "multi-plot" ) @@ -600,15 +608,19 @@ async def update_selected_runs(self, *args): indexes = self.ui.run_tableview.selectedIndexes() uids = [i.siblingAtColumn(col_idx).data() for i in indexes] # Get selected runs from the database - task = self.db_task(self.db.load_selected_runs(uids), "update selected runs") - self.selected_runs = await task - # Update the necessary UI elements - await self.update_1d_signals() - await self.update_2d_signals() - await self.update_metadata() - await self.update_1d_plot() - await self.update_2d_plot() - await self.update_multi_plot() + with self.busy_hints(run_widgets=True, run_table=False, filter_widgets=False): + task = self.db_task( + self.db.load_selected_runs(uids), "update selected runs" + ) + self.selected_runs = await task + # Update the necessary UI elements + await self.update_multi_signals() + await self.update_1d_signals() + await self.update_2d_signals() + await self.update_metadata() + await self.update_1d_plot() + await self.update_2d_plot() + await self.update_multi_plot() def filters(self, *args): new_filters = { diff --git a/src/firefly/run_client.py b/src/firefly/run_client.py index 252a604c..a610784d 100644 --- a/src/firefly/run_client.py +++ b/src/firefly/run_client.py @@ -5,7 +5,6 @@ import numpy as np import pandas as pd -from qtpy.QtCore import Signal from tiled import queries from haven import exceptions @@ -17,14 +16,6 @@ class DatabaseWorker: selected_runs: Sequence = [] - # Signals - all_runs_changed = Signal(list) - selected_runs_changed = Signal(list) - distinct_fields_changed = Signal(dict) - new_message = Signal(str, int) - db_op_started = Signal() - db_op_ended = Signal(list) # (list of exceptions thrown) - def __init__(self, catalog=None, *args, **kwargs): if catalog is None: catalog = Catalog() diff --git a/src/firefly/tests/test_run_browser.py b/src/firefly/tests/test_run_browser.py index 6435cbde..34e1eb91 100644 --- a/src/firefly/tests/test_run_browser.py +++ b/src/firefly/tests/test_run_browser.py @@ -10,46 +10,51 @@ @pytest.fixture() -def display(affapp, catalog): +async def display(qtbot, catalog): display = RunBrowserDisplay(root_node=catalog) + qtbot.addWidget(display) display.clear_filters() - # Flush pending async coroutines - loop = asyncio.get_event_loop() - pending = asyncio.all_tasks(loop) - loop.run_until_complete(asyncio.gather(*pending)) - assert all(task.done() for task in pending), "Init tasks not complete." - # Run the test - # yield display - try: - yield display - finally: - # Cancel remaining tasks - loop = asyncio.get_event_loop() - pending = asyncio.all_tasks(loop) - loop.run_until_complete(asyncio.gather(*pending)) - assert all(task.done() for task in pending), "Shutdown tasks not complete." - - -@pytest.mark.skip(reason="There's some segmentation fault here that needs to be fixed") -def test_run_viewer_action(ffapp, monkeypatch): - monkeypatch.setattr(ffapp, "create_window", MagicMock()) - assert hasattr(ffapp, "show_run_browser_action") - ffapp.show_run_browser_action.trigger() - assert isinstance(ffapp.windows["run_browser"], MagicMock) - - -@pytest.mark.skip(reason="There's some segmentation fault here that needs to be fixed") + # Wait for the initial database load to process + await display._running_db_tasks["init_load_runs"] + await display._running_db_tasks["update_combobox_items"] + return display + + @pytest.mark.asyncio -async def test_load_runs(display): +async def test_db_task(display): + async def test_coro(): + return 15 + + result = await display.db_task(test_coro()) + assert result == 15 + + +@pytest.mark.asyncio +async def test_db_task_interruption(display, event_loop): + async def test_coro(sleep_time): + await asyncio.sleep(sleep_time) + return sleep_time + + # Create an existing task that will be cancelled + task_1 = display.db_task(test_coro(1.0), name="testing") + # Now execute another task + result = await display.db_task(test_coro(0.01), name="testing") + assert result == 0.01 + # Check that the first one was cancelled + with pytest.raises(asyncio.exceptions.CancelledError): + await task_1 + assert task_1.done() + assert task_1.cancelled() + + +def test_load_runs(display): assert display.runs_model.rowCount() > 0 assert display.ui.runs_total_label.text() == str(display.runs_model.rowCount()) -@pytest.mark.skip(reason="There's some segmentation fault here that needs to be fixed") @pytest.mark.asyncio async def test_update_selected_runs(display): # Change the proposal item - selection_model = display.ui.run_tableview.selectionModel() item = display.runs_model.item(0, 1) assert item is not None display.ui.run_tableview.selectRow(0) @@ -59,19 +64,16 @@ async def test_update_selected_runs(display): assert len(display.db.selected_runs) > 0 -@pytest.mark.skip(reason="There's some segmentation fault here that needs to be fixed") @pytest.mark.asyncio async def test_metadata(display): # Change the proposal item display.ui.run_tableview.selectRow(0) await display.update_selected_runs() # Check that the metadata was set properly in the Metadata tab - metadata_doc = display.ui.metadata_textedit.document() text = display.ui.metadata_textedit.document().toPlainText() assert "xafs_scan" in text -@pytest.mark.skip(reason="There's some segmentation fault here that needs to be fixed") @pytest.mark.asyncio async def test_1d_plot_signals(catalog, display): # Check that the 1D plot was created @@ -84,7 +86,6 @@ async def test_1d_plot_signals(catalog, display): await display.update_selected_runs() # Check signals in checkboxes for combobox in [ - display.ui.multi_signal_x_combobox, display.ui.signal_y_combobox, display.ui.signal_r_combobox, display.ui.signal_x_combobox, @@ -94,7 +95,7 @@ async def test_1d_plot_signals(catalog, display): ), f"energy_energy signal not in {combobox.objectName()}." -@pytest.mark.skip(reason="There's some segmentation fault here that needs to be fixed") +# Warns: Task was destroyed but it is pending! @pytest.mark.asyncio async def test_1d_plot_signal_memory(catalog, display): """Do we remember the signals that were previously selected.""" @@ -116,9 +117,9 @@ async def test_1d_plot_signal_memory(catalog, display): assert cb.currentText() == "energy_id_energy_readback" -@pytest.mark.skip(reason="There's some segmentation fault here that needs to be fixed") +# Warns: Task was destroyed but it is pending! @pytest.mark.asyncio -async def test_1d_hinted_signals(catalog, display, ffapp): +async def test_1d_hinted_signals(catalog, display): display.ui.plot_1d_hints_checkbox.setChecked(True) # Check that the 1D plot was created plot_widget = display.ui.plot_1d_view @@ -139,9 +140,8 @@ async def test_1d_hinted_signals(catalog, display, ffapp): ), f"unhinted signal found in {combobox.objectName()}." -@pytest.mark.skip(reason="There's some segmentation fault here that needs to be fixed") @pytest.mark.asyncio -async def test_update_1d_plot(catalog, display, ffapp): +async def test_update_1d_plot(catalog, display): # Set up some fake data run = [run async for run in catalog.values()][0] display.db.selected_runs = [run] @@ -173,7 +173,7 @@ async def test_update_1d_plot(catalog, display, ffapp): np.testing.assert_almost_equal(ydata, expected_ydata) -@pytest.mark.skip(reason="There's some segmentation fault here that needs to be fixed") +# Warns: Task was destroyed but it is pending! @pytest.mark.asyncio async def test_2d_plot_signals(catalog, display): # Check that the 1D plot was created @@ -189,7 +189,6 @@ async def test_2d_plot_signals(catalog, display): assert combobox.findText("It_net_counts") > -1 -@pytest.mark.skip(reason="There's some segmentation fault here that needs to be fixed") @pytest.mark.asyncio async def test_update_2d_plot(catalog, display): display.plot_2d_item.setRect = MagicMock() @@ -221,7 +220,6 @@ async def test_update_2d_plot(catalog, display): display.plot_2d_item.setRect.assert_called_with(-100, -80, 200, 160) -@pytest.mark.skip(reason="There's some segmentation fault here that needs to be fixed") @pytest.mark.asyncio async def test_update_multi_plot(catalog, display): run = await catalog["7d1daf1d-60c7-4aa7-a668-d1cd97e5335f"] @@ -242,7 +240,6 @@ async def test_update_multi_plot(catalog, display): # np.testing.assert_almost_equal(ydata, expected_ydata) -@pytest.mark.skip(reason="There's some segmentation fault here that needs to be fixed") @pytest.mark.asyncio async def test_filter_runs(catalog): worker = DatabaseWorker(catalog=catalog) @@ -251,7 +248,6 @@ async def test_filter_runs(catalog): assert len(runs) == 1 -@pytest.mark.skip(reason="There's some segmentation fault here that needs to be fixed") @pytest.mark.asyncio async def test_distinct_fields(catalog, display): worker = DatabaseWorker(catalog=catalog) @@ -261,33 +257,65 @@ async def test_distinct_fields(catalog, display): assert key in distinct_fields.keys() -@pytest.mark.skip(reason="There's some segmentation fault here that needs to be fixed") -@pytest.mark.asyncio -async def test_db_task(display): - async def test_coro(): - return 15 - - result = await display.db_task(test_coro()) - assert result == 15 +def test_busy_hints_run_widgets(display): + """Check that the display widgets get disabled during DB hits.""" + with display.busy_hints(run_widgets=True, run_table=False): + # Are widgets disabled in the context block? + assert not display.ui.detail_tabwidget.isEnabled() + # Are widgets re-enabled outside the context block? + assert display.ui.detail_tabwidget.isEnabled() + + +def test_busy_hints_run_table(display): + """Check that the all_runs table view gets disabled during DB hits.""" + with display.busy_hints(run_table=True, run_widgets=False): + # Are widgets disabled in the context block? + assert not display.ui.run_tableview.isEnabled() + # Are widgets re-enabled outside the context block? + assert display.ui.run_tableview.isEnabled() + + +def test_busy_hints_filters(display): + """Check that the all_runs table view gets disabled during DB hits.""" + with display.busy_hints(run_table=False, run_widgets=False, filter_widgets=True): + # Are widgets disabled in the context block? + assert not display.ui.filters_widget.isEnabled() + # Are widgets re-enabled outside the context block? + assert display.ui.filters_widget.isEnabled() + + +def test_busy_hints_status(display, mocker): + """Check that any busy_hints displays the message "Loading…".""" + spy = mocker.spy(display, "show_message") + print(spy) + with display.busy_hints(run_table=True, run_widgets=False): + # Are widgets disabled in the context block? + assert not display.ui.run_tableview.isEnabled() + assert spy.call_count == 1 + # Are widgets re-enabled outside the context block? + assert spy.call_count == 2 + assert display.ui.run_tableview.isEnabled() + + +def test_busy_hints_multiple(display): + """Check that multiple busy hints can co-exist.""" + # Next the busy_hints context to mimic multiple async calls + with display.busy_hints(run_widgets=True): + # Are widgets disabled in the outer block? + assert not display.ui.detail_tabwidget.isEnabled() + with display.busy_hints(run_widgets=True): + # Are widgets disabled in the inner block? + assert not display.ui.detail_tabwidget.isEnabled() + # Are widgets still disabled in the outer block? + assert not display.ui.detail_tabwidget.isEnabled() + # Are widgets re-enabled outside the context block? + assert display.ui.detail_tabwidget.isEnabled() -@pytest.mark.skip(reason="There's some segmentation fault here that needs to be fixed") @pytest.mark.asyncio -async def test_db_task_interruption(display, event_loop): - async def test_coro(sleep_time): - await asyncio.sleep(sleep_time) - return sleep_time - - # Create an existing task that will be cancelled - task_1 = display.db_task(test_coro(1.0), name="testing") - # Now execute another task - result = await display.db_task(test_coro(0.01), name="testing") - assert result == 0.01 - # Check that the first one was cancelled - with pytest.raises(asyncio.exceptions.CancelledError): - await task_1 - assert task_1.done() - assert task_1.cancelled() +async def test_update_combobox_items(display): + """Check that the comboboxes get the distinct filter fields.""" + assert display.ui.filter_plan_combobox.count() > 0 # ----------------------------------------------------------------------------- diff --git a/src/haven/catalog.py b/src/haven/catalog.py index ad35651a..b2b4a73f 100644 --- a/src/haven/catalog.py +++ b/src/haven/catalog.py @@ -2,6 +2,7 @@ import logging import sqlite3 import threading +import warnings from functools import partial import databroker @@ -235,7 +236,10 @@ async def hints(self): """ metadata = await self.metadata # Get hints for the independent (X) - independent = metadata["start"]["hints"]["dimensions"][0][0] + try: + independent = metadata["start"]["hints"]["dimensions"][0][0] + except (KeyError, IndexError): + warnings.warn("Could not get independent hints") # Get hints for the dependent (X) dependent = [] primary_metadata = await self.loop.run_in_executor(