diff --git a/scripts/Muon/GUI/Common/ADSHandler/muon_workspace_wrapper.py b/scripts/Muon/GUI/Common/ADSHandler/muon_workspace_wrapper.py index a8a2cabd81098925a28273f1319cd46ed84e6413..34f122eaef985169d055a8644bb817368427cee2 100644 --- a/scripts/Muon/GUI/Common/ADSHandler/muon_workspace_wrapper.py +++ b/scripts/Muon/GUI/Common/ADSHandler/muon_workspace_wrapper.py @@ -155,8 +155,7 @@ class MuonWorkspaceWrapper(object): self._workspace_name = "" self._directory_structure = "" else: - raise RuntimeWarning( - "Cannot remove workspace from ADS with name : {}".format(self._workspace_name)) + pass def add_directory_structure(self): """ diff --git a/scripts/Muon/GUI/Common/calculate_pair_and_group.py b/scripts/Muon/GUI/Common/calculate_pair_and_group.py index 167aa6ccadffa7c2083c681892b541dd5a617c16..a7c285eba27fa7810185dc44301ec0f49b7f5235 100644 --- a/scripts/Muon/GUI/Common/calculate_pair_and_group.py +++ b/scripts/Muon/GUI/Common/calculate_pair_and_group.py @@ -4,7 +4,7 @@ # NScD Oak Ridge National Laboratory, European Spallation Source # & Institut Laue - Langevin # SPDX - License - Identifier: GPL - 3.0 + -import Muon.GUI.Common.load_utils as load_utils +import Muon.GUI.Common.utilities.algorithm_utils as algorithm_utils def calculate_group_data(context, group_name): @@ -12,7 +12,7 @@ def calculate_group_data(context, group_name): params = _get_MuonGroupingCounts_parameters(context, group_name) params["InputWorkspace"] = processed_data - group_data = load_utils.run_MuonGroupingCounts(context, params) + group_data = algorithm_utils.run_MuonGroupingCounts(params) return group_data @@ -22,7 +22,7 @@ def calculate_pair_data(context, pair_name): params = _get_MuonPairingAsymmetry_parameters(context, pair_name) params["InputWorkspace"] = processed_data - pair_data = load_utils.run_MuonPairingAsymmetry(context, params) + pair_data = algorithm_utils.run_MuonPairingAsymmetry(params) return pair_data @@ -30,7 +30,7 @@ def calculate_pair_data(context, pair_name): def _run_pre_processing(context): params = context._get_pre_processing_params() params["InputWorkspace"] = context.loaded_workspace - processed_data = load_utils.run_MuonPreProcess(context, params) + processed_data = algorithm_utils.run_MuonPreProcess(params) return processed_data diff --git a/scripts/Muon/GUI/Common/context_example/context_example_view.py b/scripts/Muon/GUI/Common/context_example/context_example_view.py index c06283ec817f1f84392a68705a314f1fc742dd4e..417cd95fefdfa43592e34e794526a9dac8534a5e 100644 --- a/scripts/Muon/GUI/Common/context_example/context_example_view.py +++ b/scripts/Muon/GUI/Common/context_example/context_example_view.py @@ -11,7 +11,7 @@ from __future__ import (absolute_import, division, print_function) from PyQt4 import QtCore from PyQt4 import QtGui -from Muon.GUI.Common import table_utils +from Muon.GUI.Common.utilities import table_utils class ContextExampleView(QtGui.QWidget): diff --git a/scripts/Muon/GUI/Common/load_file_widget/view.py b/scripts/Muon/GUI/Common/load_file_widget/view.py index 52157be58c84bfa8d10142040d0a373f47f69884..57180ce4d4929bfac91972fa980d131397bd7f7f 100644 --- a/scripts/Muon/GUI/Common/load_file_widget/view.py +++ b/scripts/Muon/GUI/Common/load_file_widget/view.py @@ -1,3 +1,9 @@ +# Mantid Repository : https://github.com/mantidproject/mantid +# +# Copyright © 2018 ISIS Rutherford Appleton Laboratory UKRI, +# NScD Oak Ridge National Laboratory, European Spallation Source +# & Institut Laue - Langevin +# SPDX - License - Identifier: GPL - 3.0 + from __future__ import (absolute_import, division, print_function) from PyQt4 import QtGui @@ -77,7 +83,6 @@ class BrowseFileWidgetView(QtGui.QWidget): def show_file_browser_and_return_selection(self, file_filter, search_directories, multiple_files=False): default_directory = search_directories[0] - print("OPENING FILE BROWSER") if multiple_files: chosen_files = QtGui.QFileDialog.getOpenFileNames(self, "Select files", default_directory, file_filter) diff --git a/scripts/Muon/GUI/Common/load_run_widget/model.py b/scripts/Muon/GUI/Common/load_run_widget/load_run_model.py similarity index 100% rename from scripts/Muon/GUI/Common/load_run_widget/model.py rename to scripts/Muon/GUI/Common/load_run_widget/load_run_model.py diff --git a/scripts/Muon/GUI/Common/load_run_widget/presenter.py b/scripts/Muon/GUI/Common/load_run_widget/load_run_presenter.py similarity index 100% rename from scripts/Muon/GUI/Common/load_run_widget/presenter.py rename to scripts/Muon/GUI/Common/load_run_widget/load_run_presenter.py diff --git a/scripts/Muon/GUI/Common/load_run_widget/view.py b/scripts/Muon/GUI/Common/load_run_widget/load_run_view.py similarity index 100% rename from scripts/Muon/GUI/Common/load_run_widget/view.py rename to scripts/Muon/GUI/Common/load_run_widget/load_run_view.py diff --git a/scripts/Muon/GUI/Common/muon_data_context.py b/scripts/Muon/GUI/Common/muon_data_context.py index 7e84d777f34a8313d02878cfc8ae5f60a72ba743..92aadd7cca73754d82898414f1228c394b62b565 100644 --- a/scripts/Muon/GUI/Common/muon_data_context.py +++ b/scripts/Muon/GUI/Common/muon_data_context.py @@ -7,19 +7,22 @@ from __future__ import (absolute_import, division, print_function) -import Muon.GUI.Common.load_utils as load_utils +import Muon.GUI.Common.utilities.load_utils as load_utils +import Muon.GUI.Common.utilities.xml_utils as xml_utils +from Muon.GUI.Common.ADSHandler.muon_workspace_wrapper import MuonWorkspaceWrapper from Muon.GUI.Common.muon_group import MuonGroup from Muon.GUI.Common.muon_pair import MuonPair from Muon.GUI.Common.muon_load_data import MuonLoadData -from Muon.GUI.Common.muon_file_utils import format_run_for_file -from Muon.GUI.Common.run_string_utils import run_list_to_string -from Muon.Gui.Common.ADSHandler.workspace_naming import (get_raw_data_workspace_name, get_group_data_workspace_name, +from Muon.GUI.Common.utilities.muon_file_utils import format_run_for_file +from Muon.GUI.Common.utilities.run_string_utils import run_list_to_string + +from Muon.GUI.Common.ADSHandler.workspace_naming import (get_raw_data_workspace_name, get_group_data_workspace_name, get_pair_data_workspace_name, get_base_data_directory, get_raw_data_directory, get_group_data_directory, get_pair_data_directory) -from Muon.Gui.Common.calculate_pair_and_group import calculate_group_data, calculate_pair_data +from Muon.GUI.Common.calculate_pair_and_group import calculate_group_data, calculate_pair_data from collections import OrderedDict @@ -36,7 +39,7 @@ def get_default_grouping(instrument, main_field_direction): return [], [] instrument_directory = ConfigServiceImpl.Instance().getInstrumentDirectory() filename = instrument_directory + grouping_file - new_groups, new_pairs = load_utils.load_grouping_from_XML(filename) + new_groups, new_pairs = xml_utils.load_grouping_from_XML(filename) return new_groups, new_pairs @@ -111,7 +114,8 @@ class MuonDataContext(object): @property def instrument(self): - inst = ConfigService.getInstrument() + ConfigService['default.instrument'] = 'EMU' + inst = ConfigService.getInstrument().name() return inst @property @@ -174,7 +178,7 @@ class MuonDataContext(object): # return the first workspace in the group return self.current_data["OutputWorkspace"][0].workspace else: - return self.current_data["OutputWorkspace"].workspace + return self.current_data["OutputWorkspace"][0].workspace @property def period_string(self): @@ -263,7 +267,7 @@ class MuonDataContext(object): directory = get_base_data_directory(self) + get_group_data_directory(self) workspace = calculate_group_data(self, group_name) - self._groups[group_name].workspace = load_utils.MuonWorkspace(workspace) + self._groups[group_name].workspace = MuonWorkspaceWrapper(workspace) if show: self._groups[group_name].workspace.show(directory + name) @@ -276,13 +280,13 @@ class MuonDataContext(object): directory = get_base_data_directory(self) + get_pair_data_directory(self) workspace = calculate_pair_data(self, pair_name) - self._pairs[pair_name].workspace = load_utils.MuonWorkspace(workspace) + self._pairs[pair_name].workspace = MuonWorkspaceWrapper(workspace) if show: self._pairs[pair_name].workspace.show(directory + name) def calculate_all_groups(self): for group_name in self._groups.keys(): - calculate_group_data(group_name) + calculate_group_data(self, group_name) def set_groups_and_pairs_to_default(self): groups, pairs = get_default_grouping(self.instrument, self.main_field_direction) diff --git a/scripts/Muon/GUI/Common/utilities/load_utils.py b/scripts/Muon/GUI/Common/utilities/load_utils.py index 38bbf077d2a06f7fa97a99cfab9462ddd1059011..c2f704d73893c4cf99a26fc2eb8a023d4b2812b0 100644 --- a/scripts/Muon/GUI/Common/utilities/load_utils.py +++ b/scripts/Muon/GUI/Common/utilities/load_utils.py @@ -14,8 +14,8 @@ from mantid.simpleapi import mtd from mantid import api from mantid.kernel import ConfigServiceImpl import Muon.GUI.Common.utilities.muon_file_utils as file_utils -from Muon.GUI.Common.ADSHandler.muon_workspace_wrapper import MuonWorkspaceWrapper import Muon.GUI.Common.utilities.algorithm_utils as algorithm_utils +from Muon.GUI.Common.ADSHandler.muon_workspace_wrapper import MuonWorkspaceWrapper class LoadUtils(object): @@ -190,7 +190,7 @@ def load_dead_time_from_filename(filename): return "" assert isinstance(dead_times, ITableWorkspace) - instrument = loaded_data["OutputWorkspace"].workspace.getInstrument().getName() + instrument = loaded_data["OutputWorkspace"][0].workspace.getInstrument().getName() name = str(instrument) + file_utils.format_run_for_file(run) + "_deadTimes" api.AnalysisDataService.Instance().addOrReplace(name, dead_times) @@ -213,11 +213,10 @@ def load_workspace_from_filename(filename, load_result = _get_algorithm_properties(alg, output_properties) load_result["OutputWorkspace"] = [MuonWorkspaceWrapper(ws) for ws in load_result["OutputWorkspace"]] run = get_run_from_multi_period_data(workspace) - else: # single period data load_result = _get_algorithm_properties(alg, output_properties) - load_result["OutputWorkspace"] = MuonWorkspaceWrapper(load_result["OutputWorkspace"]) + load_result["OutputWorkspace"] = [MuonWorkspaceWrapper(load_result["OutputWorkspace"])] run = int(workspace.getRunNumber()) load_result["DataDeadTimeTable"] = load_result["DeadTimeTable"] @@ -257,19 +256,24 @@ def get_table_workspace_names_from_ADS(): def combine_loaded_runs(model, run_list): return_ws = model._loaded_data_store.get_data(run=run_list[0])["workspace"] - running_total = return_ws["OutputWorkspace"].workspace - - for run in run_list[1:]: - ws = model._loaded_data_store.get_data(run=run)["workspace"]["OutputWorkspace"].workspace - running_total = algorithm_utils.run_Plus({ - "LHSWorkspace": running_total, - "RHSWorkspace": ws, - "AllowDifferentNumberSpectra": False} - ) - # remove the single loaded filename + running_total = [] + + for index, workspace in enumerate(return_ws["OutputWorkspace"]): + running_total.append(workspace.workspace) + + for run in run_list[1:]: + ws = model._loaded_data_store.get_data(run=run)["workspace"]["OutputWorkspace"][index].workspace + running_total[index] = algorithm_utils.run_Plus({ + "LHSWorkspace": running_total[index], + "RHSWorkspace": ws, + "AllowDifferentNumberSpectra": False} + ) + # remove the single loaded filename + + for run in run_list: model._loaded_data_store.remove_data(run=run) - model._loaded_data_store.remove_data(run=run_list[0]) - return_ws["OutputWorkspace"] = MuonWorkspaceWrapper(running_total) + + return_ws["OutputWorkspace"] = [MuonWorkspaceWrapper(running_total_period) for running_total_period in running_total] model._loaded_data_store.add_data(run=flatten_run_list(run_list), workspace=return_ws, filename="Co-added") diff --git a/scripts/Muon/GUI/Common/utilities/run_string_utils.py b/scripts/Muon/GUI/Common/utilities/run_string_utils.py index 5840c097ac3e97c8dd081c552069a63328a4a700..566fac649c164704ec2ca3d25f3fc5020a782f39 100644 --- a/scripts/Muon/GUI/Common/utilities/run_string_utils.py +++ b/scripts/Muon/GUI/Common/utilities/run_string_utils.py @@ -92,8 +92,15 @@ def run_string_to_list(run_string): if len(runs) == 1: run_list += [int(runs)] else: - range_max = int(split_runs[-1]) - range_min = int(split_runs[0]) + range_max = split_runs[-1] + range_min = split_runs[0] + max_length = len(range_max) + min_length = len(range_min) + if(max_length < min_length): + range_max = range_min[:max_length - min_length + 1] + range_max + + range_max = int(range_max) + range_min = int(range_min) if (range_max - range_min) > max_run_list_size: raise IndexError( "Too many runs ({}) must be <{}".format(range_max - range_min, max_run_list_size)) diff --git a/scripts/Muon/GUI/MuonAnalysis/load_widget/__init__.py b/scripts/Muon/GUI/MuonAnalysis/load_widget/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/scripts/Muon/GUI/MuonAnalysis/load_widget/load_widget.py b/scripts/Muon/GUI/MuonAnalysis/load_widget/load_widget.py new file mode 100644 index 0000000000000000000000000000000000000000..49076132a2fe7675e97b77d04d1a2aa677a10e91 --- /dev/null +++ b/scripts/Muon/GUI/MuonAnalysis/load_widget/load_widget.py @@ -0,0 +1,37 @@ +# Mantid Repository : https://github.com/mantidproject/mantid +# +# Copyright © 2018 ISIS Rutherford Appleton Laboratory UKRI, +# NScD Oak Ridge National Laboratory, European Spallation Source +# & Institut Laue - Langevin +# SPDX - License - Identifier: GPL - 3.0 + +from Muon.GUI.Common.load_file_widget.model import BrowseFileWidgetModel +from Muon.GUI.Common.load_file_widget.view import BrowseFileWidgetView +from Muon.GUI.Common.load_file_widget.presenter import BrowseFileWidgetPresenter + +from Muon.GUI.Common.load_run_widget.load_run_model import LoadRunWidgetModel +from Muon.GUI.Common.load_run_widget.load_run_view import LoadRunWidgetView +from Muon.GUI.Common.load_run_widget.load_run_presenter import LoadRunWidgetPresenter + +from Muon.GUI.MuonAnalysis.load_widget.load_widget_model import LoadWidgetModel +from Muon.GUI.MuonAnalysis.load_widget.load_widget_view import LoadWidgetView +from Muon.GUI.MuonAnalysis.load_widget.load_widget_presenter import LoadWidgetPresenter + + +class LoadWidget(object): + def __init__(self, loaded_data, instrument, parent): + # set up the views + self.load_file_view = BrowseFileWidgetView(parent) + self.load_run_view = LoadRunWidgetView(parent) + self.load_widget_view = LoadWidgetView(parent=parent, + load_file_view=self.load_file_view, + load_run_view=self.load_run_view) + self.load_widget = LoadWidgetPresenter(self.load_widget_view, + LoadWidgetModel(loaded_data)) + + self.file_widget = BrowseFileWidgetPresenter(self.load_file_view, BrowseFileWidgetModel(loaded_data)) + self.run_widget = LoadRunWidgetPresenter(self.load_run_view, LoadRunWidgetModel(loaded_data)) + + self.load_widget.set_load_file_widget(self.file_widget) + self.load_widget.set_load_run_widget(self.run_widget) + + self.load_widget.set_current_instrument(instrument) diff --git a/scripts/Muon/GUI/MuonAnalysis/load_widget/load_widget_model.py b/scripts/Muon/GUI/MuonAnalysis/load_widget/load_widget_model.py new file mode 100644 index 0000000000000000000000000000000000000000..b2871d9b85947ba67581a57a9100060efb1475c4 --- /dev/null +++ b/scripts/Muon/GUI/MuonAnalysis/load_widget/load_widget_model.py @@ -0,0 +1,43 @@ +# Mantid Repository : https://github.com/mantidproject/mantid +# +# Copyright © 2018 ISIS Rutherford Appleton Laboratory UKRI, +# NScD Oak Ridge National Laboratory, European Spallation Source +# & Institut Laue - Langevin +# SPDX - License - Identifier: GPL - 3.0 + +from __future__ import (absolute_import, division, print_function) + +from Muon.GUI.Common.muon_load_data import MuonLoadData + + +class LoadWidgetModel(object): + """ + The model is responsible for storing the currently loaded run or runs + (both the nun numbers, filenames and workspaces) as well as loading new runs using a separate loading thread. + """ + + def __init__(self, loaded_data_store=MuonLoadData()): + self._loaded_data_store = loaded_data_store + + def add_muon_data(self, filename, workspace, run): + self._loaded_data_store.add_data(run=run, filename=filename, workspace=workspace) + + def clear_data(self): + self._loaded_data_store.clear() + + def is_filename_loaded(self, filename): + return self._loaded_data_store.contains(filename=filename) + + def is_run_loaded(self, run): + return self._loaded_data_store.contains(run=run) + + @property + def workspaces(self): + return self._loaded_data_store.get_parameter("workspace") + + @property + def runs(self): + return self._loaded_data_store.get_parameter("run") + + @property + def filenames(self): + return self._loaded_data_store.get_parameter("filename") diff --git a/scripts/Muon/GUI/MuonAnalysis/load_widget/load_widget_presenter.py b/scripts/Muon/GUI/MuonAnalysis/load_widget/load_widget_presenter.py new file mode 100644 index 0000000000000000000000000000000000000000..e4ea4c43883e9e0a3c8d385e70ea442f9ee64f97 --- /dev/null +++ b/scripts/Muon/GUI/MuonAnalysis/load_widget/load_widget_presenter.py @@ -0,0 +1,152 @@ +# Mantid Repository : https://github.com/mantidproject/mantid +# +# Copyright © 2018 ISIS Rutherford Appleton Laboratory UKRI, +# NScD Oak Ridge National Laboratory, European Spallation Source +# & Institut Laue - Langevin +# SPDX - License - Identifier: GPL - 3.0 + +from __future__ import (absolute_import, division, print_function) + +from Muon.GUI.Common.observer_pattern import Observer, Observable +from mantid import ConfigService + +CO_ADD = 'Co-Add' +SIMULTANEOUS = 'Simultaneous' + + +class LoadWidgetPresenter(object): + """ + The load widget is responsible for combining data loaded from its two sub-widgets in a systematic way + (either keeping a single workspace, or allowing multiple to be loaded at once). + + - It handles any additional load behaviours such as how to handle multiple runs. + - Handles instrument change. + + Although there is duplication of code (in the models of the subwidgets) this allows them to be + used standalone without this parent widget. + """ + + def __init__(self, view, model): + self._view = view + self._model = model + + self.load_run_widget = None + self.load_file_widget = None + + self.view.on_subwidget_loading_started(self.disable_loading) + self.view.on_subwidget_loading_finished(self.enable_loading) + + self.view.on_file_widget_data_changed(self.handle_file_widget_data_changed) + self.view.on_run_widget_data_changed(self.handle_run_widget_data_changed) + + self.view.on_clear_button_clicked(self.clear_data_and_view) + + self._view.on_multiple_loading_check_changed(self.handle_multiple_files_option_changed) + self._view.on_multiple_load_type_changed(self.handle_multiple_files_option_changed) + + self.instrumentObserver = LoadWidgetPresenter.InstrumentObserver(self) + self.loadNotifier = LoadWidgetPresenter.LoadNotifier(self) + + def set_load_run_widget(self, widget): + self.load_run_widget = widget + self.load_run_widget.enable_multiple_files(False) + self.load_run_widget.update_view_from_model([]) + + def set_load_file_widget(self, widget): + self.load_file_widget = widget + self.load_file_widget.enable_multiple_files(False) + self.load_file_widget.update_view_from_model([]) + + def handle_multiple_files_option_changed(self, unused=None): + if self._view.get_multiple_loading_state(): + self.enable_multiple_files(True) + else: + self.enable_multiple_files(False) + + selection = self._view.get_multiple_loading_combo_text() + + if selection == CO_ADD: + self.load_file_widget.update_multiple_loading_behaviour(CO_ADD) + self.load_run_widget.update_multiple_loading_behaviour(CO_ADD) + + if selection == SIMULTANEOUS: + self.load_file_widget.update_multiple_loading_behaviour(SIMULTANEOUS) + self.load_run_widget.update_multiple_loading_behaviour(SIMULTANEOUS) + + self.clear_data_and_view() + + def enable_multiple_files(self, enabled=True): + self.load_run_widget.enable_multiple_files(enabled) + self.load_file_widget.enable_multiple_files(enabled) + + def handle_file_widget_data_changed(self): + self.load_run_widget.update_view_from_model(self._model.runs) + self.load_file_widget.update_view_from_model(self._model.filenames) + self.loadNotifier.notify_subscribers() + + def handle_run_widget_data_changed(self): + self.load_run_widget.update_view_from_model(self._model.runs) + self.load_file_widget.update_view_from_model(self._model.filenames) + self.loadNotifier.notify_subscribers() + + def disable_loading(self): + self.load_run_widget.disable_loading() + self.load_file_widget.disable_loading() + self._view.disable_loading() + + def enable_loading(self): + self.load_run_widget.enable_loading() + self.load_file_widget.enable_loading() + self._view.enable_loading() + + def show(self): + self._view.show() + + def clear_data(self): + self._model.clear_data() + + def clear_data_and_view(self): + self._model.clear_data() + self.handle_run_widget_data_changed() + self.handle_run_widget_data_changed() + self.load_run_widget.set_current_instrument(ConfigService.getInstrument().name()) + self.loadNotifier.notify_subscribers() + + @property + def view(self): + return self._view + + def set_current_instrument(self, instrument): + self._model._loaded_data_store.instrument = instrument + self.load_file_widget.set_current_instrument(instrument) + self.load_run_widget.set_current_instrument(instrument) + + def update_new_instrument(self, instrument): + self.clear_data_and_view() + self.set_current_instrument(instrument) + + class LoadNotifier(Observable): + """ + Notify when loaded data changes from file widget or run widget, or when clear button is pressed. + """ + + def __init__(self, outer): + Observable.__init__(self) + self.outer = outer # handle to containing class + + def notify_subscribers(self, arg=None): + # AnalysisDataService.clear() + for workspace, run in zip(self.outer._model.workspaces, self.outer._model.runs): + for i, single_ws in enumerate(workspace['OutputWorkspace']): + name = str(run) + "_period_" + str(i) + single_ws.show(name) + + Observable.notify_subscribers(self, arg) + + class InstrumentObserver(Observer): + + def __init__(self, outer): + self.outer = outer + + def update(self, observable, arg): + print("update called : ", arg) + self.outer.update_new_instrument(arg) diff --git a/scripts/Muon/GUI/MuonAnalysis/load_widget/load_widget_view.py b/scripts/Muon/GUI/MuonAnalysis/load_widget/load_widget_view.py new file mode 100644 index 0000000000000000000000000000000000000000..a334211478cd3ed00821039ae6a73dbb6dd1b462 --- /dev/null +++ b/scripts/Muon/GUI/MuonAnalysis/load_widget/load_widget_view.py @@ -0,0 +1,128 @@ +# Mantid Repository : https://github.com/mantidproject/mantid +# +# Copyright © 2018 ISIS Rutherford Appleton Laboratory UKRI, +# NScD Oak Ridge National Laboratory, European Spallation Source +# & Institut Laue - Langevin +# SPDX - License - Identifier: GPL - 3.0 + +from __future__ import (absolute_import, division, print_function) + +from PyQt4 import QtGui + + +class LoadWidgetView(QtGui.QWidget): + + def __init__(self, parent=None, load_run_view=None, load_file_view=None): + super(LoadWidgetView, self).__init__(parent) + + self.load_run_widget = load_run_view + self.load_file_widget = load_file_view + + self.setup_interface_layout() + self.set_connections() + + def setup_interface_layout(self): + + self.clear_button = QtGui.QPushButton(self) + self.clear_button.setObjectName("clearButton") + self.clear_button.setToolTip("Clear the currently loaded data") + self.clear_button.setText("Clear All") + + self.multiple_loading_label = QtGui.QLabel(self) + self.multiple_loading_label.setObjectName("multiple_loading_label") + self.multiple_loading_label.setText("Multiple loading : ") + + self.multiple_loading_check = QtGui.QCheckBox(self) + self.multiple_loading_check.setToolTip("Enable/disable loading multiple runs at once") + self.multiple_loading_check.setChecked(False) + + self.load_behaviour_combo = QtGui.QComboBox(self) + self.load_behaviour_combo.setObjectName("load_behaviour_combo") + self.load_behaviour_combo.addItem("Co-Add") + self.load_behaviour_combo.addItem("Simultaneous") + self.load_behaviour_combo.setToolTip("The behaviour of the loaded data in multiple file mode") + self.load_behaviour_combo.setEnabled(False) + + # Set the layout of the tools at the bottom of the widget + self.horizontal_layout = QtGui.QHBoxLayout() + self.horizontal_layout.setObjectName("horizontalLayout") + self.horizontal_layout.addWidget(self.clear_button) + self.horizontal_layout.addStretch(0) + self.horizontal_layout.addWidget(self.multiple_loading_label) + self.horizontal_layout.addWidget(self.multiple_loading_check) + self.horizontal_layout.addWidget(self.load_behaviour_combo) + + self.horizontal_layout.setContentsMargins(0, 0, 0, 0) + self.horizontal_layout.setMargin(0) + self.tool_widget = QtGui.QWidget(self) + self.tool_widget.setLayout(self.horizontal_layout) + + # Set the layout vertically + self.vertical_layout = QtGui.QVBoxLayout() + self.vertical_layout.addWidget(self.load_run_widget) + self.vertical_layout.addWidget(self.load_file_widget) + self.vertical_layout.addWidget(self.tool_widget) + + self.vertical_layout.addStretch(1) + + self.group = QtGui.QGroupBox("Loading") + self.group.setFlat(False) + self.setStyleSheet("QGroupBox {border: 1px solid grey;border-radius: 10px;margin-top: 1ex; margin-right: 0ex}" + "QGroupBox:title {" + 'subcontrol-origin: margin;' + "padding: 0 3px;" + 'subcontrol-position: top center;' + 'padding-top: -10px;' + 'padding-bottom: 0px;' + "padding-right: 10px;" + ' color: grey; }') + self.group.setLayout(self.vertical_layout) + + self.widget_layout = QtGui.QVBoxLayout(self) + self.widget_layout.addWidget(self.group) + self.setLayout(self.widget_layout) + + def get_multiple_loading_combo_text(self): + return str(self.load_behaviour_combo.currentText()) + + def set_connections(self): + self.on_multiple_loading_check_changed(self.change_multiple_loading_state) + + def on_multiple_loading_check_changed(self, slot): + self.multiple_loading_check.stateChanged.connect(slot) + + def on_multiple_load_type_changed(self, slot): + self.load_behaviour_combo.currentIndexChanged.connect(slot) + + def get_multiple_loading_state(self): + return self.multiple_loading_check.isChecked() + + def change_multiple_loading_state(self): + if self.get_multiple_loading_state(): + self.load_behaviour_combo.setEnabled(True) + else: + self.load_behaviour_combo.setEnabled(False) + + def on_subwidget_loading_started(self, slot): + self.load_run_widget.loadingStarted.connect(slot) + self.load_file_widget.loadingStarted.connect(slot) + + def on_subwidget_loading_finished(self, slot): + self.load_run_widget.loadingFinished.connect(slot) + self.load_file_widget.loadingFinished.connect(slot) + + def on_file_widget_data_changed(self, slot): + self.load_file_widget.dataChanged.connect(slot) + + def on_run_widget_data_changed(self, slot): + self.load_run_widget.dataChanged.connect(slot) + + def on_clear_button_clicked(self, slot): + self.clear_button.clicked.connect(slot) + + def disable_loading(self): + self.clear_button.setEnabled(False) + self.multiple_loading_check.setEnabled(False) + + def enable_loading(self): + self.clear_button.setEnabled(True) + self.multiple_loading_check.setEnabled(True) diff --git a/scripts/Muon_Analysis_2.py b/scripts/Muon_Analysis_2.py index 54d8b2cfe9ae8c3aa5b0f4a61de3dcb227fbcfc7..bf303a746bd87905381dab5be64144343b511299 100644 --- a/scripts/Muon_Analysis_2.py +++ b/scripts/Muon_Analysis_2.py @@ -11,62 +11,67 @@ import sys import PyQt4.QtGui as QtGui import PyQt4.QtCore as QtCore - -from Muon.GUI.Common.dummy_label.dummy_label_widget import DummyLabelWidget -from Muon.GUI.MuonAnalysis.dock.dock_widget import DockWidget -from Muon.GUI.Common.muon_context.muon_context import * # MuonContext +from mantid.kernel import ConfigServiceImpl +from Muon.GUI.Common.muon_data_context import MuonDataContext from save_python import getWidgetIfOpen +from Muon.GUI.MuonAnalysis.load_widget.load_widget import LoadWidget +import Muon.GUI.Common.message_box as message_box +from Muon.GUI.Common.muon_load_data import MuonLoadData -Name = "Muon_Analysis_2" -muonGUI = None +Name = "Muon_Analysis_2" -class MuonAnalysis2Gui(QtGui.QMainWindow): +muonGUI = None +SUPPORTED_FACILITIES = ["ISIS", "SmuS"] - def __init__(self, parent=None): - super(MuonAnalysis2Gui, self).__init__(parent) - self._context = MuonContext(Name) +def check_facility(): + """ + Get the currently set facility and check if it is in the list + of supported facilities, raising an AttributeError if not. + """ + current_facility = ConfigServiceImpl.Instance().getFacility().name() + if current_facility not in SUPPORTED_FACILITIES: + raise AttributeError("Your facility {} is not supported by MuonAnalysis 2.0, so you" + "will not be able to load any files. \n \n" + "Supported facilities are :" + + "\n - ".join(SUPPORTED_FACILITIES)) - self.loadWidget = DummyLabelWidget(self._context, LoadText, self) - self.dockWidget = DockWidget(self._context, self) - self.helpWidget = DummyLabelWidget(self._context, HelpText, self) +class MuonAnalysisGui(QtGui.QMainWindow): + """ + The Muon Analaysis 2.0 interface. + """ - splitter = QtGui.QSplitter(QtCore.Qt.Vertical) - splitter.addWidget(self.loadWidget.widget) - splitter.addWidget(self.dockWidget.widget) - splitter.addWidget(self.helpWidget.widget) + @staticmethod + def warning_popup(message): + message_box.warning(str(message)) - self.setCentralWidget(splitter) - self.setWindowTitle(Name) + def __init__(self, parent=None): + super(MuonAnalysisGui, self).__init__(parent) + self.setFocusPolicy(QtCore.Qt.StrongFocus) - self.dockWidget.setUpdateContext(self.update) + try: + check_facility() + except AttributeError as error: + self.warning_popup(error.args[0]) - def saveToProject(self): - return self._context.save() + # initialise the data storing classes of the interface + self.loaded_data = MuonLoadData() + self.context = MuonDataContext(load_data=self.loaded_data) - def update(self): - # update load - self.loadWidget.updateContext() - self.dockWidget.updateContext() - self.helpWidget.updateContext() + # construct all the widgets. + self.load_widget = LoadWidget(self.loaded_data, self.context.instrument, self) - self.dockWidget.loadFromContext() + splitter = QtGui.QSplitter(QtCore.Qt.Vertical) + splitter.addWidget(self.load_widget.load_widget_view) - def loadFromContext(self, project): - self._context.loadFromProject(project) - self.loadWidget.loadFromContext() - self.dockWidget.loadFromContext() - self.helpWidget.loadFromContext() + self.setCentralWidget(splitter) + self.setWindowTitle("Muon Analysis version 2") - # cancel algs if window is closed def closeEvent(self, event): - self.dockWidget.closeEvent(event) - global muonGUI - muonGUI.deleteLater() - muonGUI = None + self.load_widget = None def qapp(): @@ -85,11 +90,10 @@ def main(): return widget app = qapp() try: - muon = MuonAnalysis2Gui() + global muon + muon = MuonAnalysisGui() muon.resize(700, 700) - muon.setProperty("launcher", Name) muon.show() - muon.setAccessibleName(Name) app.exec_() return muon except RuntimeError as error: @@ -110,9 +114,11 @@ def saveToProject(): def loadFromProject(project): global muonGUI muonGUI = main() + muonGUI.dock_widget.loadFromProject(project) muonGUI.loadFromContext(project) return muonGUI + if __name__ == '__main__': muon = main() # cannot assign straight to muonGUI diff --git a/scripts/test/Muon/CMakeLists.txt b/scripts/test/Muon/CMakeLists.txt index 40fb5ca2ac4924fb228addd00f94ce716df45be2..2b13d550c8fd88fcd592e470b7cf9e22b8a96c4d 100644 --- a/scripts/test/Muon/CMakeLists.txt +++ b/scripts/test/Muon/CMakeLists.txt @@ -7,16 +7,19 @@ set ( TEST_PY_FILES AxisChangerView_test.py FFTModel_test.py FFTPresenter_test.py - load_file_widget/loadfile_model_test.py - load_file_widget/loadfile_presenter_single_file_test.py - load_file_widget/loadfile_presenter_multiple_file_test.py - load_file_widget/loadfile_view_test.py load_run_widget/loadrun_model_test.py load_run_widget/loadrun_presenter_current_run_test.py load_run_widget/loadrun_presenter_single_file_test.py load_run_widget/loadrun_presenter_multiple_file_test.py + load_file_widget/loadfile_model_test.py + load_file_widget/loadfile_presenter_single_file_test.py + load_file_widget/loadfile_presenter_multiple_file_test.py + load_file_widget/loadfile_view_test.py load_run_widget/loadrun_presenter_increment_decrement_test.py load_run_widget/loadrun_view_test.py + loading_tab/loadwidget_presenter_test.py + loading_tab/loadwidget_presenter_multiple_file_test.py + loading_tab/loadwidget_presenter_failure_test.py LoadWidgetModel_test.py LoadWidgetPresenter_test.py LoadWidgetView_test.py diff --git a/scripts/test/Muon/load_run_widget/loadrun_model_test.py b/scripts/test/Muon/load_run_widget/loadrun_model_test.py index 45d26184168dda359b3d152aa82cdd1380cbeb20..087d139ace5cd99bbe13525d312cd402c6778f9c 100644 --- a/scripts/test/Muon/load_run_widget/loadrun_model_test.py +++ b/scripts/test/Muon/load_run_widget/loadrun_model_test.py @@ -6,7 +6,7 @@ # SPDX - License - Identifier: GPL - 3.0 + import sys -from Muon.GUI.Common.load_run_widget.model import LoadRunWidgetModel +from Muon.GUI.Common.load_run_widget.load_run_model import LoadRunWidgetModel from Muon.GUI.Common.muon_load_data import MuonLoadData from Muon.GUI.Common.utilities.muon_test_helpers import IteratorWithException import unittest @@ -32,7 +32,7 @@ class LoadRunWidgetModelTest(unittest.TestCase): self.assertEqual(model.loaded_filenames, []) self.assertEqual(model.loaded_runs, []) - @mock.patch('Muon.GUI.Common.load_run_widget.model.load_utils') + @mock.patch('Muon.GUI.Common.load_run_widget.load_run_model.load_utils') def test_execute_successfully_loads_valid_files(self, load_utils_mock): # Mock the load algorithm files = [r'EMU00019489.nxs', r'EMU00019490.nxs', r'EMU00019491.nxs'] @@ -51,7 +51,7 @@ class LoadRunWidgetModelTest(unittest.TestCase): self.assertEqual(model.loaded_runs[1], 19490) self.assertEqual(model.loaded_runs[2], 19491) - @mock.patch('Muon.GUI.Common.load_run_widget.model.load_utils') + @mock.patch('Muon.GUI.Common.load_run_widget.load_run_model.load_utils') def test_model_is_cleared_correctly(self, load_utils_mock): files = [r'EMU00019489.nxs', r'EMU00019490.nxs', r'EMU00019491.nxs'] load_return_vals = [([1, 2, 3], filename, 19489 + i) for i, filename in enumerate(files)] @@ -68,7 +68,7 @@ class LoadRunWidgetModelTest(unittest.TestCase): self.assertEqual(model.loaded_filenames, []) self.assertEqual(model.loaded_runs, []) - @mock.patch('Muon.GUI.Common.load_run_widget.model.load_utils') + @mock.patch('Muon.GUI.Common.load_run_widget.load_run_model.load_utils') def test_execute_throws_if_one_file_does_not_load_correctly_but_still_loads_other_files(self, load_utils_mock): files = [r'EMU00019489.nxs', r'EMU00019490.nxs', r'EMU00019491.nxs'] load_return_vals = [([1, 2, 3], 19489 + i, filename) for i, filename in enumerate(files)] diff --git a/scripts/test/Muon/load_run_widget/loadrun_presenter_current_run_test.py b/scripts/test/Muon/load_run_widget/loadrun_presenter_current_run_test.py index adbc37f0ca23058dd72ac7a9a9a377b811b68b2d..b71d69ef04bfca1006ec0fd5dd190421a1438eec 100644 --- a/scripts/test/Muon/load_run_widget/loadrun_presenter_current_run_test.py +++ b/scripts/test/Muon/load_run_widget/loadrun_presenter_current_run_test.py @@ -5,9 +5,9 @@ # & Institut Laue - Langevin # SPDX - License - Identifier: GPL - 3.0 + import sys -from Muon.GUI.Common.load_run_widget.model import LoadRunWidgetModel -from Muon.GUI.Common.load_run_widget.view import LoadRunWidgetView -from Muon.GUI.Common.load_run_widget.presenter import LoadRunWidgetPresenter +from Muon.GUI.Common.load_run_widget.load_run_model import LoadRunWidgetModel +from Muon.GUI.Common.load_run_widget.load_run_view import LoadRunWidgetView +from Muon.GUI.Common.load_run_widget.load_run_presenter import LoadRunWidgetPresenter from Muon.GUI.Common import mock_widget from Muon.GUI.Common.muon_load_data import MuonLoadData @@ -61,7 +61,7 @@ class LoadRunWidgetLoadCurrentRunTest(unittest.TestCase): fileUtils.get_current_run_filename = mock.Mock(return_value="EMU0001234.nxs") - patcher = mock.patch('Muon.GUI.Common.load_run_widget.model.load_utils') + patcher = mock.patch('Muon.GUI.Common.load_run_widget.load_run_model.load_utils') self.addCleanup(patcher.stop) self.load_utils_patcher = patcher.start() self.load_utils_patcher.exception_message_for_failed_files.return_value = '' diff --git a/scripts/test/Muon/load_run_widget/loadrun_presenter_increment_decrement_test.py b/scripts/test/Muon/load_run_widget/loadrun_presenter_increment_decrement_test.py index ab6e993aee13c7b2229a9b3da801fe77455b3aa7..bef049ff1671ef672c2175978979ebe19f4119b7 100644 --- a/scripts/test/Muon/load_run_widget/loadrun_presenter_increment_decrement_test.py +++ b/scripts/test/Muon/load_run_widget/loadrun_presenter_increment_decrement_test.py @@ -7,9 +7,9 @@ import sys import os -from Muon.GUI.Common.load_run_widget.model import LoadRunWidgetModel -from Muon.GUI.Common.load_run_widget.view import LoadRunWidgetView -from Muon.GUI.Common.load_run_widget.presenter import LoadRunWidgetPresenter +from Muon.GUI.Common.load_run_widget.load_run_model import LoadRunWidgetModel +from Muon.GUI.Common.load_run_widget.load_run_view import LoadRunWidgetView +from Muon.GUI.Common.load_run_widget.load_run_presenter import LoadRunWidgetPresenter from Muon.GUI.Common import mock_widget from Muon.GUI.Common.muon_load_data import MuonLoadData @@ -52,7 +52,7 @@ class LoadRunWidgetIncrementDecrementSingleFileModeTest(unittest.TestCase): self.presenter.enable_multiple_files(False) self.presenter.set_current_instrument("EMU") - patcher = mock.patch('Muon.GUI.Common.load_run_widget.model.load_utils') + patcher = mock.patch('Muon.GUI.Common.load_run_widget.load_run_model.load_utils') self.addCleanup(patcher.stop) self.load_utils_patcher = patcher.start() self.load_utils_patcher.exception_message_for_failed_files.return_value = '' diff --git a/scripts/test/Muon/load_run_widget/loadrun_presenter_multiple_file_test.py b/scripts/test/Muon/load_run_widget/loadrun_presenter_multiple_file_test.py index 551849515d1be027b94b304c7aa243c5f5dc568a..4be4e419c4a2934284f7938166ee7b2c5e862d80 100644 --- a/scripts/test/Muon/load_run_widget/loadrun_presenter_multiple_file_test.py +++ b/scripts/test/Muon/load_run_widget/loadrun_presenter_multiple_file_test.py @@ -7,9 +7,10 @@ import sys import six -from Muon.GUI.Common.load_run_widget.model import LoadRunWidgetModel -from Muon.GUI.Common.load_run_widget.view import LoadRunWidgetView -from Muon.GUI.Common.load_run_widget.presenter import LoadRunWidgetPresenter +from Muon.GUI.Common.load_run_widget.load_run_model import LoadRunWidgetModel +from Muon.GUI.Common.load_run_widget.load_run_view import LoadRunWidgetView +from Muon.GUI.Common.load_run_widget.load_run_presenter import LoadRunWidgetPresenter + from Muon.GUI.Common import mock_widget from Muon.GUI.Common.muon_load_data import MuonLoadData @@ -53,7 +54,7 @@ class LoadRunWidgetIncrementDecrementMultipleFileModeTest(unittest.TestCase): self.presenter.enable_multiple_files(True) self.presenter.set_current_instrument("EMU") - patcher = mock.patch('Muon.GUI.Common.load_run_widget.model.load_utils') + patcher = mock.patch('Muon.GUI.Common.load_run_widget.load_run_model.load_utils') self.addCleanup(patcher.stop) self.load_utils_patcher = patcher.start() self.load_utils_patcher.exception_message_for_failed_files.return_value = '' diff --git a/scripts/test/Muon/load_run_widget/loadrun_presenter_single_file_test.py b/scripts/test/Muon/load_run_widget/loadrun_presenter_single_file_test.py index 57574d887e8e91ae29f310c93323611597523b7c..69bb25d0d83fd220f6cc02351c8797d807ae5665 100644 --- a/scripts/test/Muon/load_run_widget/loadrun_presenter_single_file_test.py +++ b/scripts/test/Muon/load_run_widget/loadrun_presenter_single_file_test.py @@ -7,9 +7,10 @@ import sys import os -from Muon.GUI.Common.load_run_widget.model import LoadRunWidgetModel -from Muon.GUI.Common.load_run_widget.view import LoadRunWidgetView -from Muon.GUI.Common.load_run_widget.presenter import LoadRunWidgetPresenter +from Muon.GUI.Common.load_run_widget.load_run_model import LoadRunWidgetModel +from Muon.GUI.Common.load_run_widget.load_run_view import LoadRunWidgetView +from Muon.GUI.Common.load_run_widget.load_run_presenter import LoadRunWidgetPresenter + from Muon.GUI.Common import mock_widget from Muon.GUI.Common.muon_load_data import MuonLoadData @@ -50,7 +51,7 @@ class LoadRunWidgetPresenterTest(unittest.TestCase): self.presenter.enable_multiple_files(False) self.presenter.set_current_instrument("EMU") - patcher = mock.patch('Muon.GUI.Common.load_run_widget.model.load_utils') + patcher = mock.patch('Muon.GUI.Common.load_run_widget.load_run_model.load_utils') self.addCleanup(patcher.stop) self.load_utils_patcher = patcher.start() self.load_utils_patcher.exception_message_for_failed_files.return_value = '' diff --git a/scripts/test/Muon/load_run_widget/loadrun_view_test.py b/scripts/test/Muon/load_run_widget/loadrun_view_test.py index 28ebbc61702c56dc6b2c467fff4d01214dd6a843..2588cb058ddc88bdc1c20d6a7d4086396b102bf2 100644 --- a/scripts/test/Muon/load_run_widget/loadrun_view_test.py +++ b/scripts/test/Muon/load_run_widget/loadrun_view_test.py @@ -1,4 +1,4 @@ -from Muon.GUI.Common.load_run_widget.view import LoadRunWidgetView +from Muon.GUI.Common.load_run_widget.load_run_view import LoadRunWidgetView import unittest from Muon.GUI.Common import mock_widget diff --git a/scripts/test/Muon/loading_tab/loadwidget_presenter_failure_test.py b/scripts/test/Muon/loading_tab/loadwidget_presenter_failure_test.py new file mode 100644 index 0000000000000000000000000000000000000000..e55a78f89d7b1753e94fa6bd224c7ba417394308 --- /dev/null +++ b/scripts/test/Muon/loading_tab/loadwidget_presenter_failure_test.py @@ -0,0 +1,171 @@ +# Mantid Repository : https://github.com/mantidproject/mantid +# +# Copyright © 2018 ISIS Rutherford Appleton Laboratory UKRI, +# NScD Oak Ridge National Laboratory, European Spallation Source +# & Institut Laue - Langevin +# SPDX - License - Identifier: GPL - 3.0 + +import unittest + +try: + from unittest import mock +except ImportError: + import mock + +from PyQt4 import QtGui + +from Muon.GUI.MuonAnalysis.load_widget.load_widget_model import LoadWidgetModel +from Muon.GUI.MuonAnalysis.load_widget.load_widget_view import LoadWidgetView +from Muon.GUI.MuonAnalysis.load_widget.load_widget_presenter import LoadWidgetPresenter + +from Muon.GUI.Common.load_run_widget.load_run_model import LoadRunWidgetModel +from Muon.GUI.Common.load_run_widget.load_run_view import LoadRunWidgetView +from Muon.GUI.Common.load_run_widget.load_run_presenter import LoadRunWidgetPresenter + +from Muon.GUI.Common.load_file_widget.view import BrowseFileWidgetView +from Muon.GUI.Common.load_file_widget.presenter import BrowseFileWidgetPresenter +from Muon.GUI.Common.load_file_widget.model import BrowseFileWidgetModel +from Muon.GUI.Common import mock_widget + +from Muon.GUI.Common.muon_load_data import MuonLoadData +import Muon.GUI.Common.utilities.muon_file_utils as file_utils + + +class LoadRunWidgetPresenterLoadFailTest(unittest.TestCase): + def wait_for_thread(self, thread_model): + if thread_model: + thread_model._thread.wait() + self._qapp.processEvents() + + def setUp(self): + self._qapp = mock_widget.mockQapp() + + # Store an empty widget to parent all the views, and ensure they are deleted correctly + self.obj = QtGui.QWidget() + + self.popup_patcher = mock.patch('Muon.GUI.Common.thread_model.warning') + self.addCleanup(self.popup_patcher.stop) + self.popup_mock = self.popup_patcher.start() + + self.load_patcher = mock.patch('Muon.GUI.Common.load_file_widget.model.load_utils.load_workspace_from_filename') + self.addCleanup(self.load_patcher.stop) + self.load_mock = self.load_patcher.start() + + self.load_run_patcher = mock.patch( + 'Muon.GUI.Common.load_run_widget.load_run_model.load_utils.load_workspace_from_filename') + self.addCleanup(self.load_run_patcher.stop) + self.load_run_mock = self.load_run_patcher.start() + + self.data = MuonLoadData() + self.load_file_view = BrowseFileWidgetView(self.obj) + self.load_run_view = LoadRunWidgetView(self.obj) + self.load_file_model = BrowseFileWidgetModel(self.data) + self.load_run_model = LoadRunWidgetModel(self.data) + + self.model = LoadWidgetModel(self.data) + self.view = LoadWidgetView(parent=self.obj, load_run_view=self.load_run_view, + load_file_view=self.load_file_view) + + self.presenter = LoadWidgetPresenter(view=self.view, model=self.model) + self.presenter.set_load_file_widget(BrowseFileWidgetPresenter(self.load_file_view, self.load_file_model)) + self.presenter.set_load_run_widget(LoadRunWidgetPresenter(self.load_run_view, self.load_run_model)) + self.presenter.load_run_widget.set_current_instrument('EMU') + + self.presenter.load_file_widget._view.warning_popup = mock.MagicMock() + self.presenter.load_run_widget._view.warning_popup = mock.MagicMock() + + self.load_file_view.show_file_browser_and_return_selection = mock.Mock( + return_value=["C:\\dir1\\EMU0001234.nxs"]) + self.load_mock.return_value = ([1], 1234, "C:\\dir1\\EMU0001234.nxs") + self.load_run_mock.return_value = ([1], 1234, "C:\\dir1\\EMU0001234.nxs") + + self.presenter.load_file_widget.on_browse_button_clicked() + self.wait_for_thread(self.presenter.load_file_widget._load_thread) + + self.mock_loading_to_throw() + file_utils.get_current_run_filename = mock.Mock(return_value="EMU0001234.nxs") + + def tearDown(self): + self.obj = None + + def load_failure(self): + raise ValueError("Error text") + + def mock_loading_to_throw(self): + self.load_mock.side_effect = self.load_failure + self.load_run_mock.side_effect = self.load_failure + + def mock_disabling_buttons_in_run_and_file_widget(self): + self.load_run_view.disable_load_buttons = mock.Mock() + self.load_run_view.enable_load_buttons = mock.Mock() + self.load_file_view.disable_load_buttons = mock.Mock() + self.load_file_view.enable_load_buttons = mock.Mock() + + def assert_model_unchanged(self): + self.assertEqual(self.presenter._model.filenames, ["C:\\dir1\\EMU0001234.nxs"]) + self.assertEqual(self.presenter._model.workspaces, [[1]]) + self.assertEqual(self.presenter._model.runs, [1234]) + + def assert_interface_unchanged(self): + self.assertEqual(self.load_file_view.get_file_edit_text(), "C:\\dir1\\EMU0001234.nxs") + self.assertEqual(self.load_run_view.get_run_edit_text(), "1234") + + # ------------------------------------------------------------------------------------------------------------------ + # TESTS : The interface should always revert to its previous state if a load fails from anywhere in the widget + # and a warning should be shown to the user. + # ------------------------------------------------------------------------------------------------------------------ + + def test_that_if_load_fails_from_browse_that_model_and_interface_are_unchanged_from_previous_state(self): + self.presenter.load_file_widget.on_browse_button_clicked() + self.wait_for_thread(self.presenter.load_file_widget._load_thread) + + self.assert_model_unchanged() + self.assert_interface_unchanged() + + def test_that_if_load_fails_from_user_file_entry_that_model_and_interface_are_unchanged_from_previous_state(self): + self.presenter.load_file_widget.handle_file_changed_by_user() + self.wait_for_thread(self.presenter.load_file_widget._load_thread) + + self.assert_model_unchanged() + self.assert_interface_unchanged() + + def test_that_if_load_fails_from_current_run_that_model_and_interface_are_unchanged_from_previous_state(self): + self.presenter.load_run_widget.handle_load_current_run() + self.wait_for_thread(self.presenter.load_run_widget._load_thread) + + self.assert_model_unchanged() + self.assert_interface_unchanged() + + def test_that_if_load_fails_from_user_run_entry_that_model_and_interface_are_unchanged_from_previous_state(self): + self.presenter.load_run_widget.handle_run_changed_by_user() + self.wait_for_thread(self.presenter.load_run_widget._load_thread) + + self.assert_model_unchanged() + self.assert_interface_unchanged() + + def test_that_if_load_fails_from_browse_that_warning_is_displayed(self): + self.presenter.load_file_widget.on_browse_button_clicked() + self.wait_for_thread(self.presenter.load_file_widget._load_thread) + + self.assertEqual(self.presenter.load_file_widget._view.warning_popup.call_count, 1) + + def test_that_if_load_fails_from_user_file_entry_that_warning_is_displayed(self): + self.presenter.load_file_widget.handle_file_changed_by_user() + self.wait_for_thread(self.presenter.load_file_widget._load_thread) + + self.assertEqual(self.presenter.load_file_widget._view.warning_popup.call_count, 1) + + def test_that_if_load_fails_from_current_run_that_warning_is_displayed(self): + self.presenter.load_run_widget.handle_load_current_run() + self.wait_for_thread(self.presenter.load_run_widget._load_thread) + + self.assertEqual(self.presenter.load_run_widget._view.warning_popup.call_count, 1) + + def test_that_if_load_fails_from_user_run_entry_that_warning_is_displayed(self): + self.presenter.load_run_widget.handle_run_changed_by_user() + self.wait_for_thread(self.presenter.load_run_widget._load_thread) + + self.assertEqual(self.presenter.load_run_widget._view.warning_popup.call_count, 1) + + +if __name__ == '__main__': + unittest.main(buffer=False, verbosity=2) diff --git a/scripts/test/Muon/loading_tab/loadwidget_presenter_multiple_file_test.py b/scripts/test/Muon/loading_tab/loadwidget_presenter_multiple_file_test.py new file mode 100644 index 0000000000000000000000000000000000000000..921b051373cae040ab8c65546e651fd306269d4b --- /dev/null +++ b/scripts/test/Muon/loading_tab/loadwidget_presenter_multiple_file_test.py @@ -0,0 +1,114 @@ +# Mantid Repository : https://github.com/mantidproject/mantid +# +# Copyright © 2018 ISIS Rutherford Appleton Laboratory UKRI, +# NScD Oak Ridge National Laboratory, European Spallation Source +# & Institut Laue - Langevin +# SPDX - License - Identifier: GPL - 3.0 + +import unittest + +try: + from unittest import mock +except ImportError: + import mock + +from PyQt4 import QtGui + +from Muon.GUI.MuonAnalysis.load_widget.load_widget_model import LoadWidgetModel +from Muon.GUI.MuonAnalysis.load_widget.load_widget_view import LoadWidgetView +from Muon.GUI.MuonAnalysis.load_widget.load_widget_presenter import LoadWidgetPresenter + +from Muon.GUI.Common.load_run_widget.load_run_model import LoadRunWidgetModel +from Muon.GUI.Common.load_run_widget.load_run_view import LoadRunWidgetView +from Muon.GUI.Common.load_run_widget.load_run_presenter import LoadRunWidgetPresenter + +from Muon.GUI.Common.load_file_widget.view import BrowseFileWidgetView +from Muon.GUI.Common.load_file_widget.presenter import BrowseFileWidgetPresenter +from Muon.GUI.Common.load_file_widget.model import BrowseFileWidgetModel +from Muon.GUI.Common.muon_load_data import MuonLoadData +from Muon.GUI.Common import mock_widget +from mantid.api import FileFinder +from mantid import ConfigService + + +class LoadRunWidgetPresenterMultipleFileTest(unittest.TestCase): + def wait_for_thread(self, thread_model): + if thread_model: + thread_model._thread.wait() + self._qapp.processEvents() + + def setUp(self): + self._qapp = mock_widget.mockQapp() + # Store an empty widget to parent all the views, and ensure they are deleted correctly + self.obj = QtGui.QWidget() + + self.data = MuonLoadData() + self.load_file_view = BrowseFileWidgetView(self.obj) + self.load_run_view = LoadRunWidgetView(self.obj) + self.load_file_model = BrowseFileWidgetModel(self.data) + self.load_run_model = LoadRunWidgetModel(self.data) + + self.view = LoadWidgetView(parent=self.obj, load_file_view=self.load_file_view, + load_run_view=self.load_run_view) + self.presenter = LoadWidgetPresenter(self.view, LoadWidgetModel(self.data)) + self.presenter.set_load_file_widget(BrowseFileWidgetPresenter(self.load_file_view, self.load_file_model)) + self.presenter.set_load_run_widget(LoadRunWidgetPresenter(self.load_run_view, self.load_run_model)) + + self.presenter.load_file_widget._view.warning_popup = mock.MagicMock() + self.presenter.load_run_widget._view.warning_popup = mock.MagicMock() + + self.view.multiple_loading_check.setCheckState(1) + self.presenter.handle_multiple_files_option_changed() + + self.runs = [15196, 15197] + self.workspaces = [{'OutputWorkspace': mock.MagicMock()} for _ in self.runs] + self.filenames = FileFinder.findRuns('MUSR00015196.nxs, MUSR00015197.nxs') + + ConfigService['default.instrument'] = 'MUSR' + + def tearDown(self): + self.obj = None + + def assert_model_contains_correct_loaded_data(self): + # use sorted due to threads finishing at different times + self.assertEqual(sorted(self.presenter._model.filenames), ['Co-added']) + self.assertEqual(sorted(self.presenter._model.runs), [sorted(self.runs)]) + + def assert_interface_contains_correct_runs_and_files(self): + self.assertEqual(self.load_file_view.get_file_edit_text(), ";".join(['Co-added'])) + self.assertEqual(self.load_run_view.get_run_edit_text(), "15196-15197") + + # ------------------------------------------------------------------------------------------------------------------ + # TESTS : The loading of multiple files is supported by the widget + # ------------------------------------------------------------------------------------------------------------------ + + def test_that_loading_multiple_files_via_browse_sets_model_and_interface_correctly(self): + self.load_file_view.show_file_browser_and_return_selection = mock.Mock( + return_value=self.filenames) + + self.presenter.load_file_widget.on_browse_button_clicked() + self.wait_for_thread(self.presenter.load_file_widget._load_thread) + + self.assert_model_contains_correct_loaded_data() + self.assert_interface_contains_correct_runs_and_files() + + def test_that_loading_multiple_files_via_user_input_files_sets_model_and_interface_correctly(self): + self.load_file_view.file_path_edit.setText(';'.join(self.filenames)) + + self.load_file_view.file_path_edit.returnPressed.emit() + self.wait_for_thread(self.presenter.load_file_widget._load_thread) + + self.assert_model_contains_correct_loaded_data() + self.assert_interface_contains_correct_runs_and_files() + + def test_that_loading_multiple_files_via_user_input_runs_sets_model_and_interface_correctly(self): + self.load_run_view.run_edit.setText("15196-97") + + self.load_run_view.run_edit.returnPressed.emit() + self.wait_for_thread(self.presenter.load_run_widget._load_thread) + + self.assert_model_contains_correct_loaded_data() + self.assert_interface_contains_correct_runs_and_files() + + +if __name__ == '__main__': + unittest.main(buffer=False, verbosity=2) diff --git a/scripts/test/Muon/loading_tab/loadwidget_presenter_test.py b/scripts/test/Muon/loading_tab/loadwidget_presenter_test.py new file mode 100644 index 0000000000000000000000000000000000000000..9a476b836450c8d704922f6178f25ee189d30797 --- /dev/null +++ b/scripts/test/Muon/loading_tab/loadwidget_presenter_test.py @@ -0,0 +1,241 @@ +# Mantid Repository : https://github.com/mantidproject/mantid +# +# Copyright © 2018 ISIS Rutherford Appleton Laboratory UKRI, +# NScD Oak Ridge National Laboratory, European Spallation Source +# & Institut Laue - Langevin +# SPDX - License - Identifier: GPL - 3.0 + +import unittest + +try: + from unittest import mock +except ImportError: + import mock + +from PyQt4 import QtGui + +from Muon.GUI.MuonAnalysis.load_widget.load_widget_model import LoadWidgetModel +from Muon.GUI.MuonAnalysis.load_widget.load_widget_view import LoadWidgetView +from Muon.GUI.MuonAnalysis.load_widget.load_widget_presenter import LoadWidgetPresenter + +from Muon.GUI.Common.load_run_widget.load_run_model import LoadRunWidgetModel +from Muon.GUI.Common.load_run_widget.load_run_view import LoadRunWidgetView +from Muon.GUI.Common.load_run_widget.load_run_presenter import LoadRunWidgetPresenter + +from Muon.GUI.Common.load_file_widget.view import BrowseFileWidgetView +from Muon.GUI.Common.load_file_widget.presenter import BrowseFileWidgetPresenter +from Muon.GUI.Common.load_file_widget.model import BrowseFileWidgetModel +from Muon.GUI.Common import mock_widget + +from Muon.GUI.Common.muon_load_data import MuonLoadData +import Muon.GUI.Common.utilities.muon_file_utils as file_utils +from mantid.api import FileFinder + + +class LoadRunWidgetPresenterTest(unittest.TestCase): + def wait_for_thread(self, thread_model): + if thread_model: + thread_model._thread.wait() + self._qapp.processEvents() + + def setUp(self): + self._qapp = mock_widget.mockQapp() + # Store an empty widget to parent all the views, and ensure they are deleted correctly + self.obj = QtGui.QWidget() + + self.data = MuonLoadData() + self.load_file_view = BrowseFileWidgetView(self.obj) + self.load_run_view = LoadRunWidgetView(self.obj) + self.load_file_model = BrowseFileWidgetModel(self.data) + self.load_run_model = LoadRunWidgetModel(self.data) + + self.presenter = LoadWidgetPresenter( + LoadWidgetView(parent=self.obj, load_file_view=self.load_file_view, load_run_view=self.load_run_view), + LoadWidgetModel(self.data)) + self.presenter.set_load_file_widget(BrowseFileWidgetPresenter(self.load_file_view, self.load_file_model)) + self.presenter.set_load_run_widget(LoadRunWidgetPresenter(self.load_run_view, self.load_run_model)) + + self.filepath = FileFinder.findRuns('MUSR00022725.nxs')[0] + + self.load_patcher = mock.patch('Muon.GUI.Common.load_file_widget.model.load_utils.load_workspace_from_filename') + self.addCleanup(self.load_patcher.stop) + self.load_mock = self.load_patcher.start() + + self.load_run_patcher = mock.patch( + 'Muon.GUI.Common.load_run_widget.load_run_model.load_utils.load_workspace_from_filename') + self.addCleanup(self.load_run_patcher.stop) + self.load_run_mock = self.load_run_patcher.start() + + self.mock_loading_from_browse([1], "C:\dir1\dir2\dir3\EMU0001234.nxs", 1234) + file_utils.get_current_run_filename = mock.Mock(return_value="C:\dir1\dir2\dir3\EMU0001234.nxs") + + self.presenter.load_file_widget._view.warning_popup = mock.MagicMock() + self.presenter.load_run_widget._view.warning_popup = mock.MagicMock() + + self.popup_patcher = mock.patch('Muon.GUI.Common.thread_model.warning') + self.addCleanup(self.popup_patcher.stop) + self.popup_mock = self.popup_patcher.start() + + def tearDown(self): + self.obj = None + + def mock_loading_from_browse(self, workspace, filename, run): + self.load_file_view.show_file_browser_and_return_selection = mock.Mock( + return_value=[filename]) + self.load_mock.return_value = (workspace, run, filename) + self.load_run_mock.return_value = (workspace, run, filename) + + def mock_loading_from_current_run(self, workspace, filename, run): + file_utils.get_current_run_filename = mock.Mock(return_value=filename) + self.load_run_mock.return_value = (workspace, run, filename) + self.load_mock.return_value = (workspace, run, filename) + + def mock_user_input_single_run(self, workspace, filename, run): + self.load_run_view.get_run_edit_text = mock.Mock(return_value=str(run)) + self.load_run_mock.return_value = (workspace, run, filename) + self.load_mock.return_value = (workspace, run, filename) + + def mock_user_input_single_file(self, workspace, filename, run): + self.load_run_mock.return_value = (workspace, run, filename) + self.load_mock.return_value = (workspace, run, filename) + + self.load_file_view.get_file_edit_text = mock.Mock( + return_value=filename) + + def mock_disabling_buttons_in_run_and_file_widget(self): + self.load_run_view.disable_load_buttons = mock.Mock() + self.load_run_view.enable_load_buttons = mock.Mock() + self.load_file_view.disable_load_buttons = mock.Mock() + self.load_file_view.enable_load_buttons = mock.Mock() + + # ------------------------------------------------------------------------------------------------------------------ + # TESTS : The interface should correctly load/display data via the run widget (load current run and user input run) + # and via the file widget (browse button or user enters file) + # ------------------------------------------------------------------------------------------------------------------ + + def test_that_loading_single_file_via_browse_sets_model_data(self): + self.presenter.load_file_widget.on_browse_button_clicked() + self.wait_for_thread(self.presenter.load_file_widget._load_thread) + + self.assertEqual(self.presenter._model.filenames, ["C:\dir1\dir2\dir3\EMU0001234.nxs"]) + self.assertEqual(self.presenter._model.workspaces, [[1]]) + self.assertEqual(self.presenter._model.runs, [1234]) + + def test_that_loading_single_file_via_browse_sets_run_and_file_view(self): + self.presenter.load_file_widget.on_browse_button_clicked() + self.wait_for_thread(self.presenter.load_file_widget._load_thread) + + self.assertEqual(self.load_file_view.get_file_edit_text(), "C:\dir1\dir2\dir3\EMU0001234.nxs") + self.assertEqual(self.load_run_view.get_run_edit_text(), "1234") + + def test_that_loading_via_browse_disables_all_load_buttons(self): + self.mock_disabling_buttons_in_run_and_file_widget() + + self.presenter.load_file_widget.on_browse_button_clicked() + self.wait_for_thread(self.presenter.load_file_widget._load_thread) + + self.assertEqual(self.load_file_view.disable_load_buttons.call_count, 2) + self.assertEqual(self.load_file_view.enable_load_buttons.call_count, 2) + self.assertEqual(self.load_run_view.disable_load_buttons.call_count, 1) + self.assertEqual(self.load_run_view.enable_load_buttons.call_count, 1) + + def test_that_loading_single_file_via_user_input_file_is_loaded_into_model_correctly(self): + self.mock_user_input_single_file([1], "C:\dir1\dir2\dir3\EMU0001111.nxs", 1111) + + self.presenter.load_file_widget.handle_file_changed_by_user() + self.wait_for_thread(self.presenter.load_file_widget._load_thread) + + self.assertEqual(self.presenter._model.filenames, ["C:\dir1\dir2\dir3\EMU0001111.nxs"]) + self.assertEqual(self.presenter._model.workspaces, [[1]]) + self.assertEqual(self.presenter._model.runs, [1111]) + + def test_that_loading_single_file_via_user_input_file_is_displayed_correctly_in_the_interface(self): + self.mock_user_input_single_file([1], "C:\dir1\dir2\dir3\EMU0001111.nxs", 1111) + + self.presenter.load_file_widget.handle_file_changed_by_user() + self.wait_for_thread(self.presenter.load_file_widget._load_thread) + + self.assertEqual(self.load_file_view.get_file_edit_text(), "C:\dir1\dir2\dir3\EMU0001111.nxs") + self.assertEqual(self.load_run_view.get_run_edit_text(), "1111") + + def test_that_loading_via_user_input_file_disables_all_load_buttons(self): + self.mock_user_input_single_file([1], "C:\dir1\dir2\dir3\EMU0001111.nxs", 1111) + + self.mock_disabling_buttons_in_run_and_file_widget() + + self.presenter.load_file_widget.handle_file_changed_by_user() + self.wait_for_thread(self.presenter.load_file_widget._load_thread) + + self.assertEqual(self.load_file_view.disable_load_buttons.call_count, 2) + self.assertEqual(self.load_file_view.enable_load_buttons.call_count, 2) + self.assertEqual(self.load_run_view.disable_load_buttons.call_count, 1) + self.assertEqual(self.load_run_view.enable_load_buttons.call_count, 1) + + def test_that_loading_via_load_current_run_is_loaded_into_model_correctly(self): + self.mock_loading_from_current_run([1], "\\\\EMU\\data\\EMU0083420.nxs", 83420) + + self.presenter.load_run_widget.handle_load_current_run() + self.wait_for_thread(self.presenter.load_run_widget._load_thread) + + self.assertEqual(self.presenter._model.filenames, ["\\\\EMU\\data\\EMU0083420.nxs"]) + self.assertEqual(self.presenter._model.workspaces, [[1]]) + self.assertEqual(self.presenter._model.runs, [83420]) + + def test_that_loading_via_load_current_run_is_displayed_correctly_in_the_interface(self): + self.mock_loading_from_current_run([1], "\\\\EMU\\data\\EMU0083420.nxs", 83420) + + self.presenter.load_run_widget.handle_load_current_run() + self.wait_for_thread(self.presenter.load_run_widget._load_thread) + + self.assertEqual(self.load_file_view.get_file_edit_text(), "\\\\EMU\\data\\EMU0083420.nxs") + self.assertEqual(self.load_run_view.get_run_edit_text(), "83420") + + def test_that_loading_via_load_current_run_disables_all_load_buttons(self): + self.mock_loading_from_current_run([1], "\\\\EMU\\data\\EMU0083420.nxs", 83420) + self.mock_disabling_buttons_in_run_and_file_widget() + + self.presenter.load_run_widget.handle_load_current_run() + self.wait_for_thread(self.presenter.load_run_widget._load_thread) + + self.assertEqual(self.load_file_view.disable_load_buttons.call_count, 1) + self.assertEqual(self.load_file_view.enable_load_buttons.call_count, 1) + self.assertEqual(self.load_run_view.disable_load_buttons.call_count, 3) + self.assertEqual(self.load_run_view.enable_load_buttons.call_count, 2) + + def test_that_user_input_run_is_loaded_into_model_correctly(self): + self.mock_user_input_single_run([1], "EMU00012345.nxs", 12345) + self.presenter.load_run_widget.set_current_instrument('EMU') + + self.presenter.load_run_widget.handle_run_changed_by_user() + self.wait_for_thread(self.presenter.load_run_widget._load_thread) + + self.assertEqual(self.presenter._model.filenames, ["EMU00012345.nxs"]) + self.assertEqual(self.presenter._model.workspaces, [[1]]) + self.assertEqual(self.presenter._model.runs, [12345]) + + def test_that_user_input_run_is_displayed_correctly_in_the_interface(self): + self.mock_user_input_single_run([1], "EMU00012345.nxs", 12345) + self.presenter.load_run_widget.set_current_instrument('EMU') + + self.presenter.load_run_widget.handle_run_changed_by_user() + self.wait_for_thread(self.presenter.load_run_widget._load_thread) + + self.assertEqual(self.load_file_view.get_file_edit_text(), "EMU00012345.nxs") + self.assertEqual(self.load_run_view.get_run_edit_text(), "12345") + + def test_that_loading_via_user_input_run_disables_all_load_buttons(self): + self.mock_user_input_single_run([1], "EMU00012345.nxs", 12345) + self.presenter.load_run_widget.set_current_instrument('EMU') + + self.mock_disabling_buttons_in_run_and_file_widget() + + self.presenter.load_run_widget.handle_run_changed_by_user() + self.wait_for_thread(self.presenter.load_run_widget._load_thread) + + self.assertEqual(self.load_file_view.disable_load_buttons.call_count, 1) + self.assertEqual(self.load_file_view.enable_load_buttons.call_count, 1) + self.assertEqual(self.load_run_view.disable_load_buttons.call_count, 3) + self.assertEqual(self.load_run_view.enable_load_buttons.call_count, 2) + + +if __name__ == '__main__': + unittest.main(buffer=False, verbosity=2) diff --git a/scripts/test/Muon/utilities/muon_workspace_wrapper_test.py b/scripts/test/Muon/utilities/muon_workspace_wrapper_test.py index cd774f35eccf05af81c41ef2da7cd2ef5669c771..4caf644dad5dba9d77624a2ebc0ce5e4bf8e3de2 100644 --- a/scripts/test/Muon/utilities/muon_workspace_wrapper_test.py +++ b/scripts/test/Muon/utilities/muon_workspace_wrapper_test.py @@ -195,24 +195,18 @@ class MuonWorkspaceTest(unittest.TestCase): with self.assertRaises(ValueError): workspace_handle.name = "new_name" - def test_that_hiding_workspace_more_than_once_has_no_effect_but_raises_RuntimeWarning(self): + def test_that_hiding_workspace_more_than_once_has_no_effect(self): workspace_handle = MuonWorkspaceWrapper(workspace=self.workspace) workspace_handle.show("name1") workspace_handle.hide() - with self.assertRaises(RuntimeWarning): - workspace_handle.hide() - - def test_that_if_workspace_deleted_from_ADS_then_hide_raises_a_RuntimeWarning(self): + def test_that_if_workspace_deleted_from_ADS_then_hide_does_nothing(self): workspace_handle = MuonWorkspaceWrapper(workspace=self.workspace) workspace_handle.show("name1") simpleapi.mtd.clear() - with self.assertRaises(RuntimeWarning): - workspace_handle.hide() - def test_that_hiding_workspace_deletes_groups_which_are_left_empty(self): # TODO pass diff --git a/scripts/test/Muon/utilities/run_string_utils_conversion_test.py b/scripts/test/Muon/utilities/run_string_utils_conversion_test.py index 1c3e655f0d06a13804466d2a2b8ab148777dee76..f10f2913d5b3dfcea217f247242cec05039d4624 100644 --- a/scripts/test/Muon/utilities/run_string_utils_conversion_test.py +++ b/scripts/test/Muon/utilities/run_string_utils_conversion_test.py @@ -116,6 +116,11 @@ class RunStringUtilsStringToListTest(unittest.TestCase): run_list = [1, 2, 3, 48, 49, 50] self.assertEqual(utils.run_string_to_list(run_string), run_list) + def test_run_string_allows_incomplete_upper_range(self): + run_string = '62260-66' + run_list = [62260, 62261, 62262, 62263, 62264, 62265, 62266] + self.assertEqual(utils.run_string_to_list(run_string), run_list) + if __name__ == '__main__': unittest.main()