Esempio n. 1
0
    def setup(self):
        # menus must be done first so they can be filled by the
        # plugins in register_plugin
        self.create_menus()

        # widgets
        # Log message display must be imported first
        self.set_splash("Loading message display")
        from workbench.plugins.logmessagedisplay import LogMessageDisplay
        self.messagedisplay = LogMessageDisplay(self)
        # this takes over stdout/stderr
        self.messagedisplay.register_plugin()
        self.widgets.append(self.messagedisplay)

        self.set_splash("Loading Algorithm Selector")
        from workbench.plugins.algorithmselectorwidget import AlgorithmSelector
        self.algorithm_selector = AlgorithmSelector(self)
        self.algorithm_selector.register_plugin()
        self.widgets.append(self.algorithm_selector)

        self.set_splash("Loading Plot Selector")
        from workbench.plugins.plotselectorwidget import PlotSelector
        self.plot_selector = PlotSelector(self)
        self.plot_selector.register_plugin()
        self.widgets.append(self.plot_selector)

        self.set_splash("Loading code editing widget")
        from workbench.plugins.editor import MultiFileEditor
        self.editor = MultiFileEditor(self)
        self.editor.register_plugin()
        self.widgets.append(self.editor)

        self.set_splash("Loading IPython console")
        from workbench.plugins.jupyterconsole import JupyterConsole
        self.ipythonconsole = JupyterConsole(self)
        self.ipythonconsole.register_plugin()
        self.widgets.append(self.ipythonconsole)

        from workbench.plugins.workspacewidget import WorkspaceWidget
        self.workspacewidget = WorkspaceWidget(self)
        self.workspacewidget.register_plugin()
        self.widgets.append(self.workspacewidget)

        # Set up the project, recovery and interface manager objects
        self.project = Project(GlobalFigureManager, find_all_windows_that_are_savable)
        self.project_recovery = ProjectRecovery(globalfiguremanager=GlobalFigureManager,
                                                multifileinterpreter=self.editor.editors,
                                                main_window=self)

        self.interface_executor = PythonCodeExecution()
        self.interface_executor.sig_exec_error.connect(lambda errobj: logger.warning(str(errobj)))
        self.interface_manager = InterfaceManager()

        # uses default configuration as necessary
        self.readSettings(CONF)
        self.config_updated()

        self.setup_layout()
        self.create_actions()
Esempio n. 2
0
    def setUp(self):
        self.pr = ProjectRecovery(multifileinterpreter=None)

        # Make absolutely sure that the workbench-recovery directory is cleared.
        if os.path.exists(self.pr.recovery_directory):
            shutil.rmtree(self.pr.recovery_directory)

        # Set up some checkpoints
        self.setup_some_checkpoints()

        self.pr._make_process_from_pid = mock.MagicMock()
        self.pr._is_mantid_workbench_process = mock.MagicMock(
            return_value=True)
        self.prm = ProjectRecoveryModel(self.pr, mock.MagicMock())
Esempio n. 3
0
    def setup(self):
        # menus must be done first so they can be filled by the
        # plugins in register_plugin
        self.create_menus()

        # widgets
        # Log message display must be imported first
        self.set_splash("Loading message display")
        from workbench.plugins.logmessagedisplay import LogMessageDisplay
        self.messagedisplay = LogMessageDisplay(self)
        # this takes over stdout/stderr
        self.messagedisplay.register_plugin()
        self.widgets.append(self.messagedisplay)

        self.set_splash("Loading Algorithm Selector")
        from workbench.plugins.algorithmselectorwidget import AlgorithmSelector
        self.algorithm_selector = AlgorithmSelector(self)
        self.algorithm_selector.register_plugin()
        self.widgets.append(self.algorithm_selector)

        self.set_splash("Loading Plot Selector")
        from workbench.plugins.plotselectorwidget import PlotSelector
        self.plot_selector = PlotSelector(self)
        self.plot_selector.register_plugin()
        self.widgets.append(self.plot_selector)

        self.set_splash("Loading code editing widget")
        from workbench.plugins.editor import MultiFileEditor
        self.editor = MultiFileEditor(self)
        self.editor.register_plugin()
        self.widgets.append(self.editor)

        self.set_splash("Loading IPython console")
        from workbench.plugins.jupyterconsole import JupyterConsole
        self.ipythonconsole = JupyterConsole(self)
        self.ipythonconsole.register_plugin()
        self.widgets.append(self.ipythonconsole)

        from workbench.plugins.workspacewidget import WorkspaceWidget
        self.workspacewidget = WorkspaceWidget(self)
        self.workspacewidget.register_plugin()
        self.widgets.append(self.workspacewidget)

        # Set up the project, recovery and interface manager objects
        self.project = Project(GlobalFigureManager, find_all_windows_that_are_savable)
        self.project_recovery = ProjectRecovery(globalfiguremanager=GlobalFigureManager,
                                                multifileinterpreter=self.editor.editors,
                                                main_window=self)

        self.interface_executor = PythonCodeExecution()
        self.interface_executor.sig_exec_error.connect(lambda errobj: logger.warning(str(errobj)))
        self.interface_manager = InterfaceManager()

        # uses default configuration as necessary
        self.readSettings(CONF)
        self.config_updated()

        self.setup_layout()
        self.create_actions()
Esempio n. 4
0
    def setUp(self):
        self.multifileinterpreter = mock.MagicMock()
        self.pr = ProjectRecovery(self.multifileinterpreter)
        self.working_directory = tempfile.mkdtemp()
        # Make sure there is actually a different modified time on the files
        self.firstPath = tempfile.mkdtemp()
        self.secondPath = tempfile.mkdtemp()
        self.thirdPath = tempfile.mkdtemp()

        # offset the date modified stamps in the past in case future modified dates
        # cause any problems
        finalFileDateTime = datetime.datetime.fromtimestamp(os.path.getmtime(self.thirdPath))

        dateoffset = finalFileDateTime - datetime.timedelta(hours=2)
        modTime = time.mktime(dateoffset.timetuple())
        os.utime(self.firstPath, (modTime, modTime))
        dateoffset = finalFileDateTime - datetime.timedelta(hours=1)
        modTime = time.mktime(dateoffset.timetuple())
        os.utime(self.secondPath, (modTime, modTime))
    def setUp(self):
        self.pr = ProjectRecovery(multifileinterpreter=None)

        # Make absolutely sure that the workbench-recovery directory is cleared.
        if os.path.exists(self.pr.recovery_directory):
            shutil.rmtree(self.pr.recovery_directory)

        # Set up some checkpoints
        self.setup_some_checkpoints()

        self.pr._make_process_from_pid = mock.MagicMock()
        self.pr._is_mantid_workbench_process = mock.MagicMock(return_value=True)
        self.prm = ProjectRecoveryModel(self.pr, mock.MagicMock())
Esempio n. 6
0
class ProjectRecoveryModelTest(unittest.TestCase):
    def setUp(self):
        self.pr = ProjectRecovery(multifileinterpreter=None)

        # Make absolutely sure that the workbench-recovery directory is cleared.
        if os.path.exists(self.pr.recovery_directory):
            shutil.rmtree(self.pr.recovery_directory)

        # Set up some checkpoints
        self.setup_some_checkpoints()

        self.pr._make_process_from_pid = mock.MagicMock()
        self.pr._is_mantid_workbench_process = mock.MagicMock(
            return_value=True)
        self.prm = ProjectRecoveryModel(self.pr, mock.MagicMock())

    def tearDown(self):
        # Make sure to clear the hostname layer between tests
        ADS.clear()

        if os.path.exists(self.pid):
            shutil.rmtree(self.pid)

    def setup_some_checkpoints(self):
        self.pr._spin_off_another_time_thread = mock.MagicMock()
        directory = self.pr.recovery_directory_hostname

        # Add a numbered folder for the pid
        self.pid = os.path.join(directory, "3000000")
        if not os.path.exists(self.pid):
            os.makedirs(self.pid)
        self.pr._recovery_directory_pid = self.pid

        # Add 5 workspaces
        for ii in range(0, 5):
            CreateSampleWorkspace(OutputWorkspace=str(ii))

        self.pr.saver._spin_off_another_time_thread = mock.MagicMock()
        self.pr.recovery_save()

    def test_find_number_of_workspaces_in_directory(self):
        # Expect 0 as just checkpoints
        self.assertEqual(
            self.prm.find_number_of_workspaces_in_directory(self.pid), 0)

        self.assertTrue(os.path.exists(self.pid))
        list_dir = os.listdir(self.pid)
        list_dir.sort()
        self.assertEqual(
            self.prm.find_number_of_workspaces_in_directory(
                os.path.join(self.pid, list_dir[0])), 5)

    def test_get_row_as_string(self):
        row = self.prm.rows[0]
        self.assertEqual(self.prm.get_row(row[0]), row)

    def test_get_row_as_int(self):
        row = self.prm.rows[0]
        self.assertEqual(self.prm.get_row(0), row)

    def test_get_row_as_string_not_found(self):
        row = ["", "", ""]
        self.assertEqual(self.prm.get_row("asdadasdasd"), row)

    def test_start_mantid_normally(self):
        self.prm.start_mantid_normally()
        self.assertEqual(self.prm.presenter.close_view.call_count, 1)

    def test_recover_selected_checkpoint(self):
        checkpoint = os.listdir(self.pid)[0]
        self.prm._start_recovery_of_checkpoint = mock.MagicMock()
        self.prm.recover_selected_checkpoint(checkpoint)

        self.assertEqual(
            1,
            self.prm.presenter.change_start_mantid_to_cancel_label.call_count)
        self.assertEqual(1, self.prm._start_recovery_of_checkpoint.call_count)

    def test_open_selected_in_editor(self):
        checkpoint = os.listdir(self.pid)[0]
        self.prm.project_recovery.open_checkpoint_in_script_editor = mock.MagicMock(
        )
        self.prm.open_selected_in_editor(checkpoint)

        self.assertEqual(
            1, self.prm.project_recovery.open_checkpoint_in_script_editor.
            call_count)
        self.assertEqual(
            self.prm.project_recovery.open_checkpoint_in_script_editor.
            call_args, mock.call(os.path.join(self.pid, checkpoint)))

    def test_decide_last_checkpoint(self):
        CreateSampleWorkspace(OutputWorkspace="6")
        self.pr.recovery_save()

        checkpoints = os.listdir(self.pid)
        checkpoints.sort()

        last_checkpoint = self.prm.decide_last_checkpoint()
        self.assertEqual(checkpoints[-1], os.path.basename(last_checkpoint))

    def test_fill_rows(self):
        # wait a second so that we can add a second checkpoint with a different name, because the checkpoints differ at
        # most by a second.
        time.sleep(1)

        CreateSampleWorkspace(OutputWorkspace="6")
        self.pr.recovery_save()

        self.prm.fill_rows()

        checkpoints = os.listdir(self.pid)
        checkpoints.sort()

        self.assertEqual(["", "", ""], self.prm.rows[2])
        self.assertEqual([checkpoints[0].replace("T", " "), "5", "No"],
                         self.prm.rows[1])
        self.assertEqual([checkpoints[1].replace("T", " "), "6", "No"],
                         self.prm.rows[0])

    def test_get_number_of_checkpoints(self):
        self.assertEqual(int(ConfigService.getString(NO_OF_CHECKPOINTS_KEY)),
                         self.prm.get_number_of_checkpoints())

    def test_update_checkpoint_tried(self):
        checkpoints = os.listdir(self.pid)

        self.assertEqual(self.prm.rows[0][2], "No")
        self.prm._update_checkpoint_tried(
            os.path.join(self.pid, checkpoints[0]))
        self.assertEqual(self.prm.rows[0][2], "Yes")
Esempio n. 7
0
class ProjectRecoveryTest(unittest.TestCase):
    def setUp(self):
        self.multifileinterpreter = mock.MagicMock()
        self.pr = ProjectRecovery(self.multifileinterpreter)
        self.working_directory = tempfile.mkdtemp()
        # Make sure there is actually a different modified time on the files
        self.firstPath = tempfile.mkdtemp()
        self.secondPath = tempfile.mkdtemp()
        self.thirdPath = tempfile.mkdtemp()

        # offset the date modified stamps in the past in case future modified dates
        # cause any problems
        finalFileDateTime = datetime.datetime.fromtimestamp(os.path.getmtime(self.thirdPath))

        dateoffset = finalFileDateTime - datetime.timedelta(hours=2)
        modTime = time.mktime(dateoffset.timetuple())
        os.utime(self.firstPath, (modTime, modTime))
        dateoffset = finalFileDateTime - datetime.timedelta(hours=1)
        modTime = time.mktime(dateoffset.timetuple())
        os.utime(self.secondPath, (modTime, modTime))

    def tearDown(self):
        ADS.clear()
        if os.path.exists(self.pr.recovery_directory_hostname):
            shutil.rmtree(self.pr.recovery_directory_hostname)

        if os.path.exists(self.working_directory):
            shutil.rmtree(self.working_directory)

        if os.path.exists(self.firstPath):
            shutil.rmtree(self.firstPath)

        if os.path.exists(self.secondPath):
            shutil.rmtree(self.secondPath)

        if os.path.exists(self.thirdPath):
            shutil.rmtree(self.thirdPath)

    def test_constructor_settings_are_set(self):
        # Test the paths set in the constructor that are generated.
        self.assertEqual(self.pr.recovery_directory,
                         os.path.join(ConfigService.getAppDataDirectory(), "workbench-recovery"))
        self.assertEqual(self.pr.recovery_directory_hostname,
                         os.path.join(ConfigService.getAppDataDirectory(), "workbench-recovery", socket.gethostname()))
        self.assertEqual(self.pr.recovery_directory_pid,
                         os.path.join(ConfigService.getAppDataDirectory(), "workbench-recovery", socket.gethostname(),
                                      str(os.getpid())))
        self.assertEqual(self.pr.recovery_order_workspace_history_file,
                         os.path.join(ConfigService.getAppDataDirectory(), "ordered_recovery.py"))

        # Test config service values
        self.assertEqual(self.pr.time_between_saves, int(ConfigService[SAVING_TIME_KEY]))
        self.assertEqual(self.pr.maximum_num_checkpoints, int(ConfigService[NO_OF_CHECKPOINTS_KEY]))
        self.assertEqual(self.pr.recovery_enabled, ("true" == ConfigService[RECOVERY_ENABLED_KEY].lower()))

    def test_start_recovery_thread_if_thread_on_is_true(self):
        self.pr._timer_thread = mock.MagicMock()
        self.pr.thread_on = True
        self.pr.recovery_enabled = True

        self.pr.start_recovery_thread()

        self.assertEqual(self.pr._timer_thread.start.call_count, 0)

    def test_remove_empty_dir(self):
        if not os.path.exists(self.pr.recovery_directory):
            os.makedirs(self.pr.recovery_directory)

        empty = os.path.join(self.pr.recovery_directory, "empty")
        os.mkdir(os.path.join(self.pr.recovery_directory, "empty"))

        # Feed parent to temp directory here, on Linux = '/tmp'
        parent_path, _ = os.path.split(empty)
        self.pr._remove_empty_folders_from_dir(parent_path)

        self.assertTrue(not os.path.exists(empty))

    @mock.patch('workbench.projectrecovery.projectrecovery.logger')
    def test_remove_empty_dir_logs_outside_of_workbench_directory(self, logger_mock):
        os.mkdir(os.path.join(self.working_directory, "temp"))
        self.pr._remove_empty_folders_from_dir(self.working_directory)

        logger_mock.warning.assert_called_once()

    @mock.patch('workbench.projectrecovery.projectrecovery.logger')
    def test_remove_all_folders_from_dir_outside_of_mantid_dir_logs_warning(self, logger_mock):
        self.pr._remove_directory_and_directory_trees(self.working_directory)

        logger_mock.warning.assert_called_once()

    def test_remove_all_folders_from_dir_doesnt_raise_inside_of_mantid_dir(self):
        temp_dir = os.path.join(self.pr.recovery_directory, "tempDir")
        os.mkdir(temp_dir)

        self.pr._remove_directory_and_directory_trees(temp_dir)

        self.assertTrue(not os.path.exists(temp_dir))

    @unittest.skipIf(is_macOS(), "Can be unreliable on macOS and is a test of logic not OS capability")
    def test_sort_paths_by_last_modified(self):
        paths = [self.secondPath, self.thirdPath, self.firstPath]
        paths = self.pr.sort_by_last_modified(paths)

        self.assertListEqual(paths, [self.firstPath, self.secondPath, self.thirdPath])

    def test_get_pid_folder_to_be_used_to_load_a_checkpoint_from(self):
        self.pr._make_process_from_pid = mock.MagicMock()
        self.pr._is_mantid_workbench_process = mock.MagicMock(return_value=True)
        # Needs to be a high number outside of possible pids
        one = os.path.join(self.pr.recovery_directory_hostname, "10000000")
        two = os.path.join(self.pr.recovery_directory_hostname, "20000000")

        # Make the first pid folder, neither pid will be in use but it should select the oldest first
        os.makedirs(one)
        time.sleep(0.01)
        os.makedirs(two)

        result = self.pr.get_pid_folder_to_load_a_checkpoint_from()
        self.assertEqual(one, result)

    def test_get_pid_folder_returns_pid_if_access_denied(self):
        pid = os.path.join(self.pr.recovery_directory_hostname, "10000000")
        os.makedirs(pid)
        with mock.patch('psutil.Process.cmdline', side_effect=psutil.AccessDenied()):
            result = self.pr.get_pid_folder_to_load_a_checkpoint_from()
        self.assertEqual(pid, result)

    def test_list_dir_full_path(self):
        one = os.path.join(self.working_directory, "10000000")
        two = os.path.join(self.working_directory, "20000000")
        os.makedirs(one)
        os.makedirs(two)

        # There is no concern for list order in this equality assertion
        self.assertCountEqual([one, two], self.pr.listdir_fullpath(self.working_directory))

    def test_recovery_save_when_nothing_is_present(self):
        self.pr.saver._spin_off_another_time_thread = mock.MagicMock()
        self.assertIsNone(self.pr.recovery_save())

    def test_check_for_recovery_where_no_checkpoints_exist(self):
        self.assertTrue(not self.pr.check_for_recover_checkpoint())

    @staticmethod
    def create_checkpoints_at_directory(directory, number):
        for ii in range(0, number):
            os.makedirs(os.path.join(directory, (str(ii) + "00000000"), "spare_dir"))

    def test_check_for_recovery_when_2_instances_exist_with_2_checkpoints(self):
        # Doesn't do recovery so returns false

        # Create the 2 checkpoints
        self.create_checkpoints_at_directory(self.pr.recovery_directory_hostname, 2)
        self.pr._number_of_other_workbench_processes = mock.MagicMock(return_value=2)

        self.assertTrue(not self.pr.check_for_recover_checkpoint())

    def test_check_for_recovery_when_2_instances_exist_with_3_checkpoints(self):
        # Does recovery so returns true
        # Create the 3 checkpoints
        self.create_checkpoints_at_directory(self.pr.recovery_directory_hostname, 3)
        self.pr._number_of_other_workbench_processes = mock.MagicMock(return_value=2)

        self.assertTrue(self.pr.check_for_recover_checkpoint())

    def test_remove_oldest_checkpoints_when_no_errors_present(self):
        self.pr._recovery_directory_pid = self.working_directory
        self.pr._remove_directory_and_directory_trees = mock.MagicMock()

        for ii in range(0, self.pr.maximum_num_checkpoints+1):
            os.mkdir(os.path.join(self.working_directory, "dir"+str(ii)))
            time.sleep(0.01)

        self.pr.remove_oldest_checkpoints()

        # Now should have had a call made to delete working_directory + dir0
        self.pr._remove_directory_and_directory_trees.assert_called_with(os.path.join(self.working_directory, "dir0"))

    @mock.patch('workbench.projectrecovery.projectrecovery.logger')
    def test_remove_oldest_checkpoints_logs_when_directory_not_removable(self, logger_mock):
        self.pr._recovery_directory_pid = self.working_directory
        self.pr._remove_directory_and_directory_trees = mock.MagicMock(side_effect=OSError("Error removing directory"))

        for ii in range(0, self.pr.maximum_num_checkpoints+1):
            os.mkdir(os.path.join(self.working_directory, "dir"+str(ii)))
            time.sleep(0.01)

        self.pr.remove_oldest_checkpoints()

        # Now should have had a call made to delete working_directory + dir0
        self.pr._remove_directory_and_directory_trees.assert_called_with(os.path.join(self.working_directory, "dir0"))
        logger_mock.warning.assert_called_once()

    def test_clear_all_unused_checkpoints_called_with_none_and_only_one_user(self):
        self.pr._remove_directory_and_directory_trees = mock.MagicMock()
        os.makedirs(self.pr.recovery_directory_hostname)

        self.pr.clear_all_unused_checkpoints()

        self.pr._remove_directory_and_directory_trees.assert_called_with(self.pr.recovery_directory_hostname,
                                                                         ignore_errors=True)

    def test_clear_all_unused_checkpoints_called_with_none_and_multiple_users(self):
        self.pr._remove_directory_and_directory_trees = mock.MagicMock()
        os.makedirs(self.pr.recovery_directory_hostname)
        user = os.path.join(self.pr.recovery_directory, "dimitar")
        if os.path.exists(user):
            shutil.rmtree(user)
        os.makedirs(user)

        self.pr.clear_all_unused_checkpoints()

        self.pr._remove_directory_and_directory_trees.assert_called_with(self.pr.recovery_directory_hostname,
                                                                         ignore_errors=True)

    def test_clear_all_unused_checkpoints_called_with_not_none(self):
        self.pr._remove_directory_and_directory_trees = mock.MagicMock()
        os.makedirs(self.pr.recovery_directory_hostname)

        self.pr.clear_all_unused_checkpoints(pid_dir=self.pr.recovery_directory_hostname)

        self.pr._remove_directory_and_directory_trees.assert_called_with(self.pr.recovery_directory_hostname,
                                                                         ignore_errors=True)

    def _repair_checkpoint_checkpoints_setup(self, checkpoint1, checkpoint2, pid, pid2):
        if os.path.exists(pid):
            shutil.rmtree(pid)
        os.makedirs(pid)
        os.makedirs(checkpoint1)
        if os.path.exists(pid2):
            shutil.rmtree(pid2)
        os.makedirs(pid2)
        os.makedirs(checkpoint2)

        # Add a lock file to checkpoint 1
        open(os.path.join(checkpoint1, self.pr.lock_file_name), 'a').close()

        # Add one workspace to the checkpoint and change modified dates to older than a month
        os.utime(pid2, (1, 1))

    def _repair_checkpoints_assertions(self, checkpoint1, checkpoint2, pid, pid2):
        # None of the checkpoints should exist after the call. Thus the PID folder should be deleted and thus ignored.
        directory_removal_calls = [mock.call(os.path.join(self.pr.recovery_directory_hostname, '200000'),
                                             ignore_errors=True),
                                   mock.call(os.path.join(self.pr.recovery_directory_hostname, "1000000", "check1"),
                                             ignore_errors=True)]

        self.pr._remove_directory_and_directory_trees.assert_has_calls(directory_removal_calls)

        empty_file_calls = [mock.call(self.pr.recovery_directory_hostname)]
        self.pr._remove_empty_folders_from_dir.assert_has_calls(empty_file_calls)

        self.assertTrue(os.path.exists(checkpoint1))
        self.assertTrue(os.path.exists(pid2))
        self.assertTrue(os.path.exists(checkpoint2))
        self.assertTrue(os.path.exists(pid))

    def test_repair_checkpoints(self):
        pid = os.path.join(self.pr.recovery_directory_hostname, "1000000")
        checkpoint1 = os.path.join(pid, "check1")
        pid2 = os.path.join(self.pr.recovery_directory_hostname, "200000")
        checkpoint2 = os.path.join(pid, "check3")
        self._repair_checkpoint_checkpoints_setup(checkpoint1, checkpoint2, pid, pid2)
        self.pr._remove_directory_and_directory_trees = mock.MagicMock()
        self.pr._remove_empty_folders_from_dir = mock.MagicMock()

        self.pr.repair_checkpoints()

        self._repair_checkpoints_assertions(checkpoint1, checkpoint2, pid, pid2)

        self.pr._remove_empty_folders_from_dir(self.pr.recovery_directory_hostname)

    def test_find_checkpoints_older_than_a_month(self):
        pid = os.path.join(self.pr.recovery_directory_hostname, "1000000")
        if os.path.exists(pid):
            shutil.rmtree(pid)

        os.makedirs(pid)
        os.utime(pid, (1, 1))

        self.assertEqual([pid], self.pr._find_checkpoints_older_than_a_month([pid]))

    def test_find_checkpoints_which_are_locked(self):
        pid = os.path.join(self.pr.recovery_directory_hostname, "1000000")
        if os.path.exists(pid):
            shutil.rmtree(pid)
        checkpoint1 = os.path.join(pid, "check1")

        os.makedirs(pid)
        os.makedirs(checkpoint1)
        # Add a lock file to checkpoint 1
        open(os.path.join(checkpoint1, self.pr.lock_file_name), 'a').close()

        self.assertEqual([checkpoint1], self.pr._find_checkpoints_which_are_locked([pid]))
class ProjectRecoverySaverTest(unittest.TestCase):
    def setUp(self):
        self.working_directory = tempfile.mkdtemp()

        self.pr = ProjectRecovery(None)
        self.pr_saver = self.pr.saver

    def tearDown(self):
        ADS.clear()
        if os.path.exists(self.pr.recovery_directory_hostname):
            shutil.rmtree(self.pr.recovery_directory_hostname)

        if os.path.exists(self.working_directory):
            shutil.rmtree(self.working_directory)

    def test_add_lock_file(self):
        self.pr_saver._add_lock_file(self.working_directory)
        self.assertTrue(
            os.path.exists(
                os.path.join(self.working_directory, self.pr.lock_file_name)))

    def test_remove_lock_file(self):
        self.pr_saver._add_lock_file(self.working_directory)
        self.assertTrue(
            os.path.exists(
                os.path.join(self.working_directory, self.pr.lock_file_name)))

        self.pr_saver._remove_lock_file(self.working_directory)
        self.assertTrue(not os.path.exists(
            os.path.join(self.working_directory, self.pr.lock_file_name)))

    def test_recovery_save_when_nothing_is_present(self):
        self.pr.saver._spin_off_another_time_thread = mock.MagicMock()
        self.assertIsNone(self.pr.recovery_save())

    def test_recovery_save_with_just_workspaces(self):
        CreateSampleWorkspace(OutputWorkspace="ws")
        self.pr_saver._spin_off_another_time_thread = mock.MagicMock()

        # Do save
        self.pr_saver.recovery_save()

        # Check 0.py was made
        checkpoint = self.pr.listdir_fullpath(
            self.pr.recovery_directory_pid)[0]
        self.assertTrue(os.path.exists(os.path.join(checkpoint, "0.py")))
        self.assertEqual(
            self.pr_saver._spin_off_another_time_thread.call_count, 1)

    @mock.patch(
        'workbench.projectrecovery.projectrecoverysaver.find_all_windows_that_are_savable'
    )
    def test_recovery_save_with_just_interfaces(self,
                                                windows_that_are_savable):
        CreateSampleWorkspace(OutputWorkspace="ws")
        windows_that_are_savable.return_value = [[
            FakeEncoder(), FakeEncoder()
        ]]
        self.pr_saver._spin_off_another_time_thread = mock.MagicMock()
        ADS.clear()

        self.pr_saver.gfm = mock.MagicMock()
        self.pr_saver.gfm.figs = {}

        self.pr_saver.recovery_save()

        # Check no 0.py was made
        checkpoint = self.pr.listdir_fullpath(
            self.pr.recovery_directory_pid)[0]
        self.assertTrue(not os.path.exists(os.path.join(checkpoint, "0.py")))
        self.assertEqual(
            self.pr_saver._spin_off_another_time_thread.call_count, 1)

        # Read the .json file and check nothing is in workspace and something is in the interfaces dictionary
        with open(
                os.path.join(checkpoint, (os.path.basename(checkpoint) +
                                          self.pr.recovery_file_ext))) as f:
            dictionary = json.load(f)
            self.assertEqual(len(dictionary["interfaces"]), 1)
            self.assertEqual(len(dictionary["workspaces"]), 0)

    def test_empty_group_workspaces(self):
        CreateSampleWorkspace(OutputWorkspace="ws")
        CreateSampleWorkspace(OutputWorkspace="ws1")
        GroupWorkspaces(OutputWorkspace="Group", InputWorkspaces="ws,ws1")
        group_workspace = ADS.retrieve("Group")
        group_workspace.remove("ws")
        group_workspace.remove("ws1")

        self.assertTrue(self.pr_saver._empty_group_workspace(group_workspace))

    @mock.patch('workbench.projectrecovery.projectrecoverysaver.Timer')
    def test_spin_of_another_thread(self, timer):
        self.pr_saver._spin_off_another_time_thread()
        timer.assert_has_calls([mock.call().start()])

    def test_save_workspaces(self):
        CreateSampleWorkspace(OutputWorkspace="ws1")
        CreateSampleWorkspace(OutputWorkspace="ws2")

        self.pr_saver._save_workspaces(self.working_directory)

        # Assert that the 0.py and 1.py that are expected are made
        self.assertTrue(
            os.path.exists(os.path.join(self.working_directory, "0.py")))
        self.assertTrue(
            os.path.exists(os.path.join(self.working_directory, "1.py")))

    def test_save_project(self):
        self.pr_saver.gfm = mock.MagicMock()
        self.pr_saver.gfm.figs = {}

        self.pr_saver._save_project(self.working_directory)

        project_file = os.path.join(self.working_directory,
                                    (os.path.basename(self.working_directory) +
                                     self.pr.recovery_file_ext))
        self.assertTrue(os.path.exists(project_file))
        with open(project_file) as f:
            dictionary = json.load(f)
            self.assertEqual(len(dictionary["interfaces"]), 0)
            self.assertEqual(len(dictionary["workspaces"]), 0)

    @mock.patch(
        'workbench.projectrecovery.projectrecoverysaver.find_all_windows_that_are_savable'
    )
    def test_recovery_save_with_both_workspace_and_interfaces(
            self, windows_that_are_savable):
        CreateSampleWorkspace(OutputWorkspace="ws")
        windows_that_are_savable.return_value = [[
            FakeEncoder(), FakeEncoder()
        ]]
        self.pr_saver._spin_off_another_time_thread = mock.MagicMock()

        self.pr_saver.gfm = mock.MagicMock()
        self.pr_saver.gfm.figs = {}

        self.pr_saver.recovery_save()

        # Check 0.py was made
        checkpoint = self.pr.listdir_fullpath(
            self.pr.recovery_directory_pid)[0]
        self.assertTrue(os.path.exists(os.path.join(checkpoint, "0.py")))
        self.assertEqual(
            self.pr_saver._spin_off_another_time_thread.call_count, 1)

        # Read the .json file and check nothing is in workspace and something is in the interfaces dictionary
        with open(
                os.path.join(checkpoint, (os.path.basename(checkpoint) +
                                          self.pr.recovery_file_ext))) as f:
            dictionary = json.load(f)
            self.assertEqual(len(dictionary["interfaces"]), 1)
            self.assertEqual(len(dictionary["workspaces"]), 1)

    def test_start_recovery_thread_if_thread_on_is_false(self):
        self.pr_saver._timer_thread = mock.MagicMock()
        self.pr_saver.thread_on = False
        self.pr_saver.pr.recovery_enabled = True

        self.pr_saver.start_recovery_thread()

        self.assertEqual(self.pr_saver._timer_thread.start.call_count, 1)

    def test_stop_recovery_thread(self):
        self.pr_saver._timer_thread = mock.MagicMock()

        self.pr_saver.stop_recovery_thread()

        self.assertEqual(self.pr_saver._timer_thread.cancel.call_count, 1)
class ProjectRecoverySaverTest(unittest.TestCase):
    def setUp(self):
        self.working_directory = tempfile.mkdtemp()

        self.pr = ProjectRecovery(None)
        self.pr_saver = self.pr.saver

    def tearDown(self):
        ADS.clear()
        if os.path.exists(self.pr.recovery_directory_hostname):
            shutil.rmtree(self.pr.recovery_directory_hostname)

        if os.path.exists(self.working_directory):
            shutil.rmtree(self.working_directory)

    def test_add_lock_file(self):
        self.pr_saver._add_lock_file(self.working_directory)
        self.assertTrue(os.path.exists(os.path.join(self.working_directory, self.pr.lock_file_name)))

    def test_remove_lock_file(self):
        self.pr_saver._add_lock_file(self.working_directory)
        self.assertTrue(os.path.exists(os.path.join(self.working_directory, self.pr.lock_file_name)))

        self.pr_saver._remove_lock_file(self.working_directory)
        self.assertTrue(not os.path.exists(os.path.join(self.working_directory, self.pr.lock_file_name)))

    def test_recovery_save_when_nothing_is_present(self):
        self.pr.saver._spin_off_another_time_thread = mock.MagicMock()
        self.assertIsNone(self.pr.recovery_save())

    def test_recovery_save_with_just_workspaces(self):
        CreateSampleWorkspace(OutputWorkspace="ws")
        self.pr_saver._spin_off_another_time_thread = mock.MagicMock()

        # Do save
        self.pr_saver.recovery_save()

        # Check 0.py was made
        checkpoint = self.pr.listdir_fullpath(self.pr.recovery_directory_pid)[0]
        self.assertTrue(os.path.exists(os.path.join(checkpoint, "0.py")))
        self.assertEqual(self.pr_saver._spin_off_another_time_thread.call_count, 1)

    @mock.patch('workbench.projectrecovery.projectrecoverysaver.find_all_windows_that_are_savable')
    def test_recovery_save_with_just_interfaces(self, windows_that_are_savable):
        CreateSampleWorkspace(OutputWorkspace="ws")
        # Return a FakeEncoder object that will return an empty dictionary
        windows_that_are_savable.return_value = [[FakeEncoder(), FakeEncoder()]]
        self.pr_saver._spin_off_another_time_thread = mock.MagicMock()
        ADS.clear()

        self.pr_saver.gfm = mock.MagicMock()
        self.pr_saver.gfm.figs = {}

        self.pr_saver.recovery_save()

        # Check no 0.py was made
        checkpoint = self.pr.listdir_fullpath(self.pr.recovery_directory_pid)[0]
        self.assertTrue(not os.path.exists(os.path.join(checkpoint, "0.py")))
        self.assertEqual(self.pr_saver._spin_off_another_time_thread.call_count, 1)

        # Read the .json file and check nothing is in workspace and something is in the interfaces dictionary
        with open(os.path.join(checkpoint, (os.path.basename(checkpoint) + self.pr.recovery_file_ext))) as f:
            dictionary = json.load(f)
            self.assertEqual(len(dictionary["interfaces"]), 1)
            self.assertEqual(len(dictionary["workspaces"]), 0)

    def test_empty_group_workspaces(self):
        CreateSampleWorkspace(OutputWorkspace="ws")
        CreateSampleWorkspace(OutputWorkspace="ws1")
        GroupWorkspaces(OutputWorkspace="Group", InputWorkspaces="ws,ws1")
        group_workspace = ADS.retrieve("Group")
        group_workspace.remove("ws")
        group_workspace.remove("ws1")

        self.assertTrue(self.pr_saver._empty_group_workspace(group_workspace))

    @mock.patch('workbench.projectrecovery.projectrecoverysaver.Timer')
    def test_spin_of_another_thread(self, timer):
        self.pr_saver._spin_off_another_time_thread()
        timer.assert_has_calls([mock.call().start()])

    def test_save_workspaces(self):
        CreateSampleWorkspace(OutputWorkspace="ws1")
        CreateSampleWorkspace(OutputWorkspace="ws2")

        self.pr_saver._save_workspaces(self.working_directory)

        # Assert that the 0.py and 1.py that are expected are made
        self.assertTrue(os.path.exists(os.path.join(self.working_directory, "0.py")))
        self.assertTrue(os.path.exists(os.path.join(self.working_directory, "1.py")))

    def test_save_project(self):
        self.pr_saver.gfm = mock.MagicMock()
        self.pr_saver.gfm.figs = {}

        self.pr_saver._save_project(self.working_directory)

        project_file = os.path.join(self.working_directory, (os.path.basename(self.working_directory) + self.pr.recovery_file_ext))
        self.assertTrue(os.path.exists(project_file))
        with open(project_file) as f:
            dictionary = json.load(f)
            self.assertEqual(len(dictionary["interfaces"]), 0)
            self.assertEqual(len(dictionary["workspaces"]), 0)

    @mock.patch('workbench.projectrecovery.projectrecoverysaver.find_all_windows_that_are_savable')
    def test_recovery_save_with_both_workspace_and_interfaces(self, windows_that_are_savable):
        CreateSampleWorkspace(OutputWorkspace="ws")
        # Return a FakeEncoder object that will return an empty dictionary
        windows_that_are_savable.return_value = [[FakeEncoder(), FakeEncoder()]]
        self.pr_saver._spin_off_another_time_thread = mock.MagicMock()

        self.pr_saver.gfm = mock.MagicMock()
        self.pr_saver.gfm.figs = {}

        self.pr_saver.recovery_save()

        # Check 0.py was made
        checkpoint = self.pr.listdir_fullpath(self.pr.recovery_directory_pid)[0]
        self.assertTrue(os.path.exists(os.path.join(checkpoint, "0.py")))
        self.assertEqual(self.pr_saver._spin_off_another_time_thread.call_count, 1)

        # Read the .json file and check nothing is in workspace and something is in the interfaces dictionary
        with open(os.path.join(checkpoint, (os.path.basename(checkpoint) + self.pr.recovery_file_ext))) as f:
            dictionary = json.load(f)
            self.assertEqual(len(dictionary["interfaces"]), 1)
            self.assertEqual(len(dictionary["workspaces"]), 1)

    def test_start_recovery_thread_if_thread_on_is_false(self):
        self.pr_saver._timer_thread = mock.MagicMock()
        self.pr_saver.thread_on = False
        self.pr_saver.recovery_enabled = True

        self.pr_saver.start_recovery_thread()

        self.assertEqual(self.pr_saver._timer_thread.start.call_count, 1)

    def test_stop_recovery_thread(self):
        self.pr_saver._timer_thread = mock.MagicMock()

        self.pr_saver.stop_recovery_thread()

        self.assertEqual(self.pr_saver._timer_thread.cancel.call_count, 1)
class ProjectRecoveryLoaderTest(unittest.TestCase):
    def setUp(self):
        self.working_directory = tempfile.mkdtemp()

        self.pr = ProjectRecovery(None)
        self.pr_loader = self.pr.loader

    def tearDown(self):
        ADS.clear()
        if os.path.exists(self.pr.recovery_directory_hostname):
            shutil.rmtree(self.pr.recovery_directory_hostname)

        if os.path.exists(self.working_directory):
            shutil.rmtree(self.working_directory)

    @mock.patch('workbench.projectrecovery.projectrecoveryloader.ProjectRecoveryPresenter')
    def test_attempt_recovery_and_recovery_passes(self, presenter):
        presenter.return_value.start_recovery_view.return_value = True
        presenter.return_value.start_recovery_failure.return_value = True
        add_main_window_mock(self.pr.loader)
        self.pr.clear_all_unused_checkpoints = mock.MagicMock()
        self.pr.start_recovery_thread = mock.MagicMock()

        self.pr.attempt_recovery()

        self.assertEqual(presenter.return_value.start_recovery_view.call_count, 1)
        self.assertEqual(presenter.return_value.start_recovery_failure.call_count, 0)
        self.assertEqual(self.pr.clear_all_unused_checkpoints.call_count, 1)
        self.assertEqual(self.pr.start_recovery_thread.call_count, 1)
        self.pr.loader.main_window.algorithm_selector.block_progress_widget_updates.assert_context_triggered()

    @mock.patch('workbench.projectrecovery.projectrecoveryloader.ProjectRecoveryPresenter')
    def test_attempt_recovery_and_recovery_fails_first_time_but_is_successful_on_failure_view(self, presenter):
        presenter.return_value.start_recovery_view.return_value = False
        presenter.return_value.start_recovery_failure.return_value = True
        add_main_window_mock(self.pr.loader)
        self.pr.clear_all_unused_checkpoints = mock.MagicMock()
        self.pr.start_recovery_thread = mock.MagicMock()

        self.pr.attempt_recovery()

        self.assertEqual(presenter.return_value.start_recovery_view.call_count, 1)
        self.assertEqual(presenter.return_value.start_recovery_failure.call_count, 1)
        self.assertEqual(self.pr.clear_all_unused_checkpoints.call_count, 1)
        self.assertEqual(self.pr.start_recovery_thread.call_count, 1)
        self.pr.loader.main_window.algorithm_selector.block_progress_widget_updates.assert_context_triggered()

    @mock.patch('workbench.projectrecovery.projectrecoveryloader.ProjectLoader')
    def test_load_project_interfaces_call(self, loader):
        loader.return_value.load_project.return_value = True

        self.pr_loader._load_project_interfaces("")

        self.assertEqual(loader.return_value.load_project.call_args, mock.call(file_name=self.pr.recovery_file_ext,
                                                                               load_workspaces=False))

    def test_compile_recovery_script(self):
        # make sure to clear out the script if it exists
        if os.path.exists(self.pr.recovery_order_workspace_history_file):
            os.remove(self.pr.recovery_order_workspace_history_file)

        # Create checkpoint
        CreateSampleWorkspace(OutputWorkspace="ws")
        self.pr.saver._spin_off_another_time_thread = mock.MagicMock()
        self.pr.recovery_save()

        # Find the checkpoint
        checkpoints = os.listdir(self.pr.recovery_directory_pid)
        checkpoint = os.path.join(self.pr.recovery_directory_pid, checkpoints[0])

        self.pr_loader._compile_recovery_script(checkpoint)

        self.assertTrue(os.path.exists(self.pr.recovery_order_workspace_history_file))

        # Confirm contents is correct
        with open(self.pr.recovery_order_workspace_history_file, 'r') as f:
            actual_file_contents = f.read()

        file_contents = ""
        # Strip out the time
        for ii in actual_file_contents:
            if ii == '#':
                break
            file_contents += ii

        self.assertEqual(file_contents,
                         "from mantid.simpleapi import *\n\nCreateSampleWorkspace(OutputWorkspace='ws') ")

    def test_load_checkpoint(self):
        # Create the checkpoint
        CreateSampleWorkspace(OutputWorkspace="ws")
        self.pr.saver._spin_off_another_time_thread = mock.MagicMock()
        self.pr.recovery_save()

        # Mock out excess function calls
        self.pr_loader.recovery_presenter = mock.MagicMock()
        self.pr_loader._open_script_in_editor_call = mock.MagicMock()
        self.pr_loader._run_script_in_open_editor = mock.MagicMock()

        # Find the checkpoint
        checkpoints = os.listdir(self.pr.recovery_directory_pid)
        checkpoint = os.path.join(self.pr.recovery_directory_pid, checkpoints[0])

        self.pr_loader.load_checkpoint(checkpoint)

        # Test the calls are made properly
        self.assertEqual(self.pr_loader._open_script_in_editor_call.call_count, 1)
        self.assertEqual(self.pr_loader._run_script_in_open_editor.call_count, 1)

    def test_open_script_in_editor(self):
        self.pr_loader.recovery_presenter = mock.MagicMock()
        self.pr_loader._open_script_in_editor_call = mock.MagicMock()

        # Ensure a script file exists
        script = os.path.join(self.working_directory, "script")
        open(script, 'a').close()

        self.pr_loader._open_script_in_editor(script)

        self.assertEqual(self.pr_loader._open_script_in_editor_call.call_count, 1)
        self.assertEqual(self.pr_loader.recovery_presenter.set_up_progress_bar.call_count, 1)
        self.assertEqual(self.pr_loader.recovery_presenter.set_up_progress_bar.call_args, mock.call(0))
Esempio n. 11
0
 def setUp(self):
     self.multifileinterpreter = mock.MagicMock()
     self.pr = ProjectRecovery(self.multifileinterpreter)
     self.working_directory = tempfile.mkdtemp()
Esempio n. 12
0
class ProjectRecoveryTest(unittest.TestCase):
    def setUp(self):
        self.multifileinterpreter = mock.MagicMock()
        self.pr = ProjectRecovery(self.multifileinterpreter)
        self.working_directory = tempfile.mkdtemp()

    def tearDown(self):
        ADS.clear()
        if os.path.exists(self.pr.recovery_directory_hostname):
            shutil.rmtree(self.pr.recovery_directory_hostname)

        if os.path.exists(self.working_directory):
            shutil.rmtree(self.working_directory)

    def test_constructor_settings_are_set(self):
        # Test the paths set in the constructor that are generated.
        self.assertEqual(self.pr.recovery_directory,
                         os.path.join(ConfigService.getAppDataDirectory(), "workbench-recovery"))
        self.assertEqual(self.pr.recovery_directory_hostname,
                         os.path.join(ConfigService.getAppDataDirectory(), "workbench-recovery", socket.gethostname()))
        self.assertEqual(self.pr.recovery_directory_pid,
                         os.path.join(ConfigService.getAppDataDirectory(), "workbench-recovery", socket.gethostname(),
                                      str(os.getpid())))
        self.assertEqual(self.pr.recovery_order_workspace_history_file,
                         os.path.join(ConfigService.getAppDataDirectory(), "ordered_recovery.py"))

        # Test config service values
        self.assertEqual(self.pr.time_between_saves, int(ConfigService[SAVING_TIME_KEY]))
        self.assertEqual(self.pr.maximum_num_checkpoints, int(ConfigService[NO_OF_CHECKPOINTS_KEY]))
        self.assertEqual(self.pr.recovery_enabled, ("true" == ConfigService[RECOVERY_ENABLED_KEY].lower()))

    def test_start_recovery_thread_if_thread_on_is_true(self):
        self.pr._timer_thread = mock.MagicMock()
        self.pr.thread_on = True
        self.pr.recovery_enabled = True

        self.pr.start_recovery_thread()

        self.assertEqual(self.pr._timer_thread.start.call_count, 0)

    def test_remove_empty_dir(self):
        if not os.path.exists(self.pr.recovery_directory):
            os.makedirs(self.pr.recovery_directory)

        empty = os.path.join(self.pr.recovery_directory, "empty")
        os.mkdir(os.path.join(self.pr.recovery_directory, "empty"))

        # Feed parent to temp directory here, on Linux = '/tmp'
        parent_path, _ = os.path.split(empty)
        self.pr._remove_empty_folders_from_dir(parent_path)

        self.assertTrue(not os.path.exists(empty))

    def test_remove_empty_dir_throws_outside_of_workbench_directory(self):
        os.mkdir(os.path.join(self.working_directory, "temp"))
        self.assertRaises(RuntimeError, self.pr._remove_empty_folders_from_dir, self.working_directory)

    def test_remove_all_folders_from_dir_raises_outside_of_mantid_dir(self):
        self.assertRaises(RuntimeError, self.pr._remove_directory_and_directory_trees, self.working_directory)

    def test_remove_all_folders_from_dir_doesnt_raise_inside_of_mantid_dir(self):
        temp_dir = os.path.join(self.pr.recovery_directory, "tempDir")
        os.mkdir(temp_dir)

        self.pr._remove_directory_and_directory_trees(temp_dir)

        self.assertTrue(not os.path.exists(temp_dir))

    @unittest.skipIf(is_macOS(), "Can be unreliable on macOS and is a test of logic not OS capability")
    def test_sort_paths_by_last_modified(self):
        # Make sure there is actually a different modified time on the files by using sleeps
        first = tempfile.mkdtemp()
        time.sleep(0.5)
        second = tempfile.mkdtemp()
        time.sleep(0.5)
        third = tempfile.mkdtemp()
        paths = [second, third, first]
        paths = self.pr.sort_by_last_modified(paths)

        self.assertListEqual(paths, [first, second, third])

    def test_get_pid_folder_to_be_used_to_load_a_checkpoint_from(self):
        self.pr._make_process_from_pid = mock.MagicMock()
        self.pr._is_mantid_workbench_process = mock.MagicMock(return_value=True)
        # Needs to be a high number outside of possible pids
        one = os.path.join(self.pr.recovery_directory_hostname, "10000000")
        two = os.path.join(self.pr.recovery_directory_hostname, "20000000")

        # Make the first pid folder, neither pid will be in use but it should select the oldest first
        os.makedirs(one)
        time.sleep(0.01)
        os.makedirs(two)

        result = self.pr.get_pid_folder_to_load_a_checkpoint_from()
        self.assertEqual(one, result)

    def test_list_dir_full_path(self):
        one = os.path.join(self.working_directory, "10000000")
        two = os.path.join(self.working_directory, "20000000")
        os.makedirs(one)
        os.makedirs(two)

        # There is no concern for list order in this equality assertion
        if sys.version_info.major < 3:
            # Python 2.7 way of doing it
            self.assertItemsEqual([one, two], self.pr.listdir_fullpath(self.working_directory))
        else:
            # Python 3.2+ way of doing it
            self.assertCountEqual([one, two], self.pr.listdir_fullpath(self.working_directory))

    def test_recovery_save_when_nothing_is_present(self):
        self.pr.saver._spin_off_another_time_thread = mock.MagicMock()
        self.assertIsNone(self.pr.recovery_save())

    def test_check_for_recovery_where_no_checkpoints_exist(self):
        self.assertTrue(not self.pr.check_for_recover_checkpoint())

    @staticmethod
    def create_checkpoints_at_directory(directory, number):
        for ii in range(0, number):
            os.makedirs(os.path.join(directory, (str(ii) + "00000000"), "spare_dir"))

    def test_check_for_recovery_when_2_instances_exist_with_2_checkpoints(self):
        # Doesn't do recovery so returns false

        # Create the 2 checkpoints
        self.create_checkpoints_at_directory(self.pr.recovery_directory_hostname, 2)
        self.pr._number_of_other_workbench_processes = mock.MagicMock(return_value=2)

        self.assertTrue(not self.pr.check_for_recover_checkpoint())

    def test_check_for_recovery_when_2_instances_exist_with_3_checkpoints(self):
        # Does recovery so returns true
        # Create the 3 checkpoints
        self.create_checkpoints_at_directory(self.pr.recovery_directory_hostname, 3)
        self.pr._number_of_other_workbench_processes = mock.MagicMock(return_value=2)

        self.assertTrue(self.pr.check_for_recover_checkpoint())

    def test_remove_oldest_checkpoints(self):
        self.pr._recovery_directory_pid = self.working_directory
        self.pr._remove_directory_and_directory_trees = mock.MagicMock()

        for ii in range(0, self.pr.maximum_num_checkpoints+1):
            os.mkdir(os.path.join(self.working_directory, "dir"+str(ii)))
            time.sleep(0.01)

        self.pr.remove_oldest_checkpoints()

        # Now should have had a call made to delete working_directory + dir0
        self.pr._remove_directory_and_directory_trees.assert_called_with(os.path.join(self.working_directory, "dir0"))

    def test_clear_all_unused_checkpoints_called_with_none_and_only_one_user(self):
        self.pr._remove_directory_and_directory_trees = mock.MagicMock()
        os.makedirs(self.pr.recovery_directory_hostname)

        self.pr.clear_all_unused_checkpoints()

        self.pr._remove_directory_and_directory_trees.assert_called_with(self.pr.recovery_directory_hostname)

    def test_clear_all_unused_checkpoints_called_with_none_and_multiple_users(self):
        self.pr._remove_directory_and_directory_trees = mock.MagicMock()
        os.makedirs(self.pr.recovery_directory_hostname)
        user = os.path.join(self.pr.recovery_directory, "dimitar")
        if os.path.exists(user):
            shutil.rmtree(user)
        os.makedirs(user)

        self.pr.clear_all_unused_checkpoints()

        self.pr._remove_directory_and_directory_trees.assert_called_with(self.pr.recovery_directory_hostname)

    def test_clear_all_unused_checkpoints_called_with_not_none(self):
        self.pr._remove_directory_and_directory_trees = mock.MagicMock()
        os.makedirs(self.pr.recovery_directory_hostname)

        self.pr.clear_all_unused_checkpoints(pid_dir=self.pr.recovery_directory_hostname)

        self.pr._remove_directory_and_directory_trees.assert_called_with(self.pr.recovery_directory_hostname)

    def _repair_checkpoint_checkpoints_setup(self, checkpoint1, checkpoint2, pid, pid2):
        if os.path.exists(pid):
            shutil.rmtree(pid)
        os.makedirs(pid)
        os.makedirs(checkpoint1)
        if os.path.exists(pid2):
            shutil.rmtree(pid2)
        os.makedirs(pid2)
        os.makedirs(checkpoint2)

        # Add a lock file to checkpoint 1
        open(os.path.join(checkpoint1, self.pr.lock_file_name), 'a').close()

        # Add one workspace to the checkpoint and change modified dates to older than a month
        os.utime(pid2, (1, 1))

    def _repair_checkpoints_assertions(self, checkpoint1, checkpoint2, pid, pid2):
        # None of the checkpoints should exist after the call. Thus the PID folder should be deleted and thus ignored.
        directory_removal_calls = [mock.call(os.path.join(self.pr.recovery_directory_hostname, '200000')),
                                   mock.call(os.path.join(self.pr.recovery_directory_hostname, "1000000", "check1"))]

        self.pr._remove_directory_and_directory_trees.assert_has_calls(directory_removal_calls)

        empty_file_calls = [mock.call(self.pr.recovery_directory_hostname)]
        self.pr._remove_empty_folders_from_dir.assert_has_calls(empty_file_calls)

        self.assertTrue(os.path.exists(checkpoint1))
        self.assertTrue(os.path.exists(pid2))
        self.assertTrue(os.path.exists(checkpoint2))
        self.assertTrue(os.path.exists(pid))

    def test_repair_checkpoints(self):
        pid = os.path.join(self.pr.recovery_directory_hostname, "1000000")
        checkpoint1 = os.path.join(pid, "check1")
        pid2 = os.path.join(self.pr.recovery_directory_hostname, "200000")
        checkpoint2 = os.path.join(pid, "check3")
        self._repair_checkpoint_checkpoints_setup(checkpoint1, checkpoint2, pid, pid2)
        self.pr._remove_directory_and_directory_trees = mock.MagicMock()
        self.pr._remove_empty_folders_from_dir = mock.MagicMock()

        self.pr.repair_checkpoints()

        self._repair_checkpoints_assertions(checkpoint1, checkpoint2, pid, pid2)

        self.pr._remove_empty_folders_from_dir(self.pr.recovery_directory_hostname)

    def test_find_checkpoints_older_than_a_month(self):
        pid = os.path.join(self.pr.recovery_directory_hostname, "1000000")
        if os.path.exists(pid):
            shutil.rmtree(pid)

        os.makedirs(pid)
        os.utime(pid, (1, 1))

        self.assertEqual([pid], self.pr._find_checkpoints_older_than_a_month([pid]))

    def test_find_checkpoints_which_are_locked(self):
        pid = os.path.join(self.pr.recovery_directory_hostname, "1000000")
        if os.path.exists(pid):
            shutil.rmtree(pid)
        checkpoint1 = os.path.join(pid, "check1")

        os.makedirs(pid)
        os.makedirs(checkpoint1)
        # Add a lock file to checkpoint 1
        open(os.path.join(checkpoint1, self.pr.lock_file_name), 'a').close()

        self.assertEqual([checkpoint1], self.pr._find_checkpoints_which_are_locked([pid]))
Esempio n. 13
0
class MainWindow(QMainWindow):
    DOCKOPTIONS = QMainWindow.AllowTabbedDocks | QMainWindow.AllowNestedDocks

    def __init__(self):
        QMainWindow.__init__(self)

        # -- instance attributes --
        self.setWindowTitle("Mantid Workbench")
        self.setObjectName("Mantid Workbench")

        # widgets
        self.messagedisplay = None
        self.ipythonconsole = None
        self.workspacewidget = None
        self.editor = None
        self.algorithm_selector = None
        self.plot_selector = None
        self.interface_manager = None
        self.widgets = []

        # Widget layout map: required for use in Qt.connection
        self._layout_widget_info = None

        # Menus
        self.file_menu = None
        self.file_menu_actions = None
        self.view_menu = None
        self.view_menu_actions = None
        self.interfaces_menu = None
        self.help_menu = None
        self.help_menu_actions = None

        # Allow splash screen text to be overridden in set_splash
        self.splash = SPLASH

        # Layout
        self.setDockOptions(self.DOCKOPTIONS)

        # Project
        self.project = None
        self.project_recovery = None

        # Interfaces
        self.interface_manager = None
        self.interface_executor = None

    def setup(self):
        # menus must be done first so they can be filled by the
        # plugins in register_plugin
        self.create_menus()

        # widgets
        # Log message display must be imported first
        self.set_splash("Loading message display")
        from workbench.plugins.logmessagedisplay import LogMessageDisplay
        self.messagedisplay = LogMessageDisplay(self)
        # this takes over stdout/stderr
        self.messagedisplay.register_plugin()
        self.widgets.append(self.messagedisplay)

        self.set_splash("Loading Algorithm Selector")
        from workbench.plugins.algorithmselectorwidget import AlgorithmSelector
        self.algorithm_selector = AlgorithmSelector(self)
        self.algorithm_selector.register_plugin()
        self.widgets.append(self.algorithm_selector)

        self.set_splash("Loading Plot Selector")
        from workbench.plugins.plotselectorwidget import PlotSelector
        self.plot_selector = PlotSelector(self)
        self.plot_selector.register_plugin()
        self.widgets.append(self.plot_selector)

        self.set_splash("Loading code editing widget")
        from workbench.plugins.editor import MultiFileEditor
        self.editor = MultiFileEditor(self)
        self.editor.register_plugin()
        self.widgets.append(self.editor)

        self.set_splash("Loading IPython console")
        from workbench.plugins.jupyterconsole import JupyterConsole
        self.ipythonconsole = JupyterConsole(self)
        self.ipythonconsole.register_plugin()
        self.widgets.append(self.ipythonconsole)

        from workbench.plugins.workspacewidget import WorkspaceWidget
        self.workspacewidget = WorkspaceWidget(self)
        self.workspacewidget.register_plugin()
        self.widgets.append(self.workspacewidget)

        # Set up the project, recovery and interface manager objects
        self.project = Project(GlobalFigureManager, find_all_windows_that_are_savable)
        self.project_recovery = ProjectRecovery(globalfiguremanager=GlobalFigureManager,
                                                multifileinterpreter=self.editor.editors,
                                                main_window=self)

        self.interface_executor = PythonCodeExecution()
        self.interface_executor.sig_exec_error.connect(lambda errobj: logger.warning(str(errobj)))
        self.interface_manager = InterfaceManager()

        # uses default configuration as necessary
        self.readSettings(CONF)
        self.config_updated()

        self.setup_layout()
        self.create_actions()

    def post_mantid_init(self):
        """Run any setup that requires mantid
        to have been initialized
        """
        self.populate_menus()
        self.algorithm_selector.refresh()

        # turn on algorithm factory notifications
        from mantid.api import AlgorithmFactory
        algorithm_factory = AlgorithmFactory.Instance()
        algorithm_factory.enableNotifications()

    def set_splash(self, msg=None):
        if not self.splash:
            return
        if msg:
            self.splash.showMessage(msg, Qt.AlignBottom | Qt.AlignLeft | Qt.AlignAbsolute,
                                    QColor(Qt.black))
        QApplication.processEvents(QEventLoop.AllEvents)

    def create_menus(self):
        self.file_menu = self.menuBar().addMenu("&File")
        self.view_menu = self.menuBar().addMenu("&View")
        self.interfaces_menu = self.menuBar().addMenu('&Interfaces')
        self.help_menu = self.menuBar().addMenu('&Help')

    def create_actions(self):
        # --- general application menu options --
        # file menu
        action_open = create_action(
            self, "Open Script", on_triggered=self.open_file,
            shortcut="Ctrl+O", shortcut_context=Qt.ApplicationShortcut)
        action_load_project = create_action(
            self, "Open Project", on_triggered=self.load_project)
        action_save_script = create_action(
            self, "Save Script", on_triggered=self.save_script,
            shortcut="Ctrl+S", shortcut_context=Qt.ApplicationShortcut)
        action_save_script_as = create_action(
            self, "Save Script as...", on_triggered=self.save_script_as)
        action_save_project = create_action(
            self, "Save Project", on_triggered=self.save_project)
        action_save_project_as = create_action(
            self, "Save Project as...", on_triggered=self.save_project_as)
        action_manage_directories = create_action(
            self, "Manage User Directories",
            on_triggered=self.open_manage_directories)
        action_settings = create_action(
            self, "Settings", on_triggered=self.open_settings_window)
        action_quit = create_action(
            self, "&Quit", on_triggered=self.close, shortcut="Ctrl+Q",
            shortcut_context=Qt.ApplicationShortcut)
        self.file_menu_actions = [action_open, action_load_project, None,
                                  action_save_script, action_save_script_as,
                                  action_save_project, action_save_project_as,
                                  None, action_settings, None,
                                  action_manage_directories, None, action_quit]
        # view menu
        action_restore_default = create_action(
            self, "Restore Default Layout",
            on_triggered=self.prep_window_for_reset,
            shortcut="Shift+F10", shortcut_context=Qt.ApplicationShortcut)

        self.view_menu_actions = [action_restore_default, None] + self.create_widget_actions()

        # help menu
        action_mantid_help = create_action(
            self, "Mantid Help", on_triggered=self.open_mantid_help,
            shortcut='F1', shortcut_context=Qt.ApplicationShortcut)
        action_algorithm_descriptions = create_action(
            self, 'Algorithm Descriptions',
            on_triggered=self.open_algorithm_descriptions_help)
        action_mantid_concepts = create_action(
            self, "Mantid Concepts", on_triggered=self.open_mantid_concepts_help)
        action_mantid_homepage = create_action(
            self, "Mantid Homepage", on_triggered=self.open_mantid_homepage)
        action_mantid_forum = create_action(
            self, "Mantid Forum", on_triggered=self.open_mantid_forum)

        self.help_menu_actions = [
            action_mantid_help, action_mantid_concepts,
            action_algorithm_descriptions, None,
            action_mantid_homepage, action_mantid_forum]

    def create_widget_actions(self):
        """
        Creates menu actions to show/hide dockable widgets.
        This uses all widgets that are in self.widgets
        :return: A list of show/hide actions for all widgets
        """
        widget_actions = []
        for widget in self.widgets:
            action = widget.dockwidget.toggleViewAction()
            widget_actions.append(action)
        return widget_actions

    def populate_menus(self):
        # Link to menus
        add_actions(self.file_menu, self.file_menu_actions)
        add_actions(self.view_menu, self.view_menu_actions)
        add_actions(self.help_menu, self.help_menu_actions)
        self.populate_interfaces_menu()

    def launch_custom_python_gui(self, filename):
        self.interface_executor.execute(open(filename).read(), filename)

    def launch_custom_cpp_gui(self, interface_name):
        interface = self.interface_manager.createSubWindow(interface_name)
        interface.setAttribute(Qt.WA_DeleteOnClose, True)
        interface.show()

    def populate_interfaces_menu(self):
        """Populate then Interfaces menu with all Python and C++ interfaces"""
        interface_dir = ConfigService['mantidqt.python_interfaces_directory']
        interfaces = self._discover_python_interfaces(interface_dir)
        interfaces.update(self._discover_cpp_interfaces())

        keys = list(interfaces.keys())
        keys.sort()
        for key in keys:
            submenu = self.interfaces_menu.addMenu(key)
            names = interfaces[key]
            names.sort()
            for name in names:
                if '.py' in name:
                    action = submenu.addAction(name.replace('.py', '').replace('_', ' '))
                    script = os.path.join(interface_dir, name)
                    action.triggered.connect(lambda checked_py, script=script: self.launch_custom_python_gui(script))
                else:
                    action = submenu.addAction(name)
                    action.triggered.connect(lambda checked_cpp, name=name:
                                             self.launch_custom_cpp_gui(name))

    def _discover_python_interfaces(self, interface_dir):
        """Return a dictionary mapping a category to a set of named Python interfaces"""
        items = ConfigService['mantidqt.python_interfaces'].split()
        # list of custom interfaces that are not qt4/qt5 compatible
        GUI_BLACKLIST = ['ISIS_Reflectometry_Old.py',
                         'Frequency_Domain_Analysis_Old.py',
                         'Frequency_Domain_Analysis.py',
                         'Elemental_Analysis.py']

        # detect the python interfaces
        interfaces = {}
        for item in items:
            key, scriptname = item.split('/')
            if not os.path.exists(os.path.join(interface_dir, scriptname)):
                logger.warning('Failed to find script "{}" in "{}"'.format(scriptname, interface_dir))
                continue
            if scriptname in GUI_BLACKLIST:
                logger.information('Not adding gui "{}"'.format(scriptname))
                continue
            interfaces.setdefault(key, []).append(scriptname)

        return interfaces

    def _discover_cpp_interfaces(self):
        """Return a dictionary mapping a category to a set of named C++ interfaces"""
        interfaces = {}
        cpp_interface_factory = UserSubWindowFactory.Instance()
        interface_names = cpp_interface_factory.keys()
        for name in interface_names:
            categories = cpp_interface_factory.categories(name)
            if len(categories) == 0:
                categories = ["General"]
            for category in categories:
                interfaces.setdefault(category, []).append(name)

        return interfaces

    def add_dockwidget(self, plugin):
        """Create a dockwidget around a plugin and add the dock to window"""
        dockwidget, location = plugin.create_dockwidget()
        self.addDockWidget(location, dockwidget)

    # ----------------------- Layout ---------------------------------

    def setup_layout(self):
        """Assume this is a first run of the application and set layouts
        accordingly"""
        self.setup_default_layouts()

    def prep_window_for_reset(self):
        """Function to reset all dock widgets to a state where they can be
        ordered by setup_default_layout"""
        for widget in self.widgets:
            widget.dockwidget.setFloating(False)  # Bring back any floating windows
            self.addDockWidget(Qt.LeftDockWidgetArea, widget.dockwidget)  # Un-tabify all widgets
        self.setup_default_layouts()

    def setup_default_layouts(self):
        """Set or reset the layouts of the child widgets"""
        # layout definition
        logmessages = self.messagedisplay
        ipython = self.ipythonconsole
        workspacewidget = self.workspacewidget
        editor = self.editor
        algorithm_selector = self.algorithm_selector
        plot_selector = self.plot_selector
        default_layout = {
            'widgets': [
                # column 0
                [[workspacewidget], [algorithm_selector, plot_selector]],
                # column 1
                [[editor, ipython]],
                # column 2
                [[logmessages]]
            ],
            'width-fraction': [0.25,  # column 0 width
                               0.50,  # column 1 width
                               0.25],  # column 2 width
            'height-fraction': [[0.5, 0.5],  # column 0 row heights
                                [1.0],  # column 1 row heights
                                [1.0]]  # column 2 row heights
        }

        with widget_updates_disabled(self):
            widgets_layout = default_layout['widgets']
            # flatten list
            widgets = [item for column in widgets_layout for row in column for item in row]
            # show everything
            for w in widgets:
                w.toggle_view(True)
            # split everything on the horizontal
            for i in range(len(widgets) - 1):
                first, second = widgets[i], widgets[i + 1]
                self.splitDockWidget(first.dockwidget, second.dockwidget,
                                     Qt.Horizontal)
            # now arrange the rows
            for column in widgets_layout:
                for i in range(len(column) - 1):
                    first_row, second_row = column[i], column[i + 1]
                    self.splitDockWidget(first_row[0].dockwidget,
                                         second_row[0].dockwidget,
                                         Qt.Vertical)
            # and finally tabify those in the same position
            for column in widgets_layout:
                for row in column:
                    for i in range(len(row) - 1):
                        first, second = row[i], row[i + 1]
                        self.tabifyDockWidget(first.dockwidget, second.dockwidget)

                    # Raise front widget per row
                    row[0].dockwidget.show()
                    row[0].dockwidget.raise_()

    # ----------------------- Events ---------------------------------
    def closeEvent(self, event):
        # Check whether or not to save project
        if not self.project.saved:
            # Offer save
            if self.project.offer_save(self):
                # Cancel has been clicked
                event.ignore()
                return

        # Close editors
        if self.editor.app_closing():
            # write out any changes to the mantid config file
            ConfigService.saveConfig(ConfigService.getUserFilename())
            # write current window information to global settings object
            self.writeSettings(CONF)
            # Close all open plots
            # We don't want this at module scope here
            import matplotlib.pyplot as plt  # noqa
            plt.close('all')

            app = QApplication.instance()
            if app is not None:
                app.closeAllWindows()

            # Kill the project recovery thread and don't restart should a save be in progress and clear out current
            # recovery checkpoint as it is closing properly
            self.project_recovery.stop_recovery_thread()
            self.project_recovery.closing_workbench = True
            self.project_recovery.remove_current_pid_folder()

            self.interface_manager.closeHelpWindow()

            event.accept()
        else:
            # Cancel was pressed when closing an editor
            event.ignore()

    # ----------------------- Slots ---------------------------------
    def open_file(self):
        # todo: when more file types are added this should
        # live in its own type
        filepath, _ = QFileDialog.getOpenFileName(self, "Open File...", "", "Python (*.py)")
        if not filepath:
            return
        self.editor.open_file_in_new_tab(filepath)

    def save_script(self):
        self.editor.save_current_file()

    def save_script_as(self):
        self.editor.save_current_file_as()

    def save_project(self):
        self.project.save()

    def save_project_as(self):
        self.project.save_as()

    def load_project(self):
        self.project.load()

    def open_manage_directories(self):
        ManageUserDirectories(self).exec_()

    def open_settings_window(self):
        settings = SettingsPresenter(self)
        settings.show()

    def config_updated(self):
        """
        Updates the widgets that depend on settings from the Workbench Config.
        """
        self.editor.load_settings_from_config(CONF)
        self.project.load_settings_from_config(CONF)

    def open_algorithm_descriptions_help(self):
        self.interface_manager.showAlgorithmHelp('')

    def open_mantid_concepts_help(self):
        self.interface_manager.showConceptHelp('')

    def open_mantid_help(self):
        self.interface_manager.showHelpPage('')

    def open_mantid_homepage(self):
        self.interface_manager.showWebPage('https://www.mantidproject.org')

    def open_mantid_forum(self):
        self.interface_manager.showWebPage('https://forum.mantidproject.org/')

    def readSettings(self, settings):
        qapp = QApplication.instance()
        qapp.setAttribute(Qt.AA_UseHighDpiPixmaps)
        if hasattr(Qt, 'AA_EnableHighDpiScaling'):
            qapp.setAttribute(Qt.AA_EnableHighDpiScaling, settings.get('high_dpi_scaling'))

        # get the saved window geometry
        window_size = settings.get('MainWindow/size')
        if not isinstance(window_size, QSize):
            window_size = QSize(*window_size)
        window_pos = settings.get('MainWindow/position')
        if not isinstance(window_pos, QPoint):
            window_pos = QPoint(*window_pos)

        # make sure main window is smaller than the desktop
        desktop = QDesktopWidget()

        # this gives the maximum screen number if the position is off screen
        screen = desktop.screenNumber(window_pos)

        # recalculate the window size
        desktop_geom = desktop.screenGeometry(screen)
        w = min(desktop_geom.size().width(), window_size.width())
        h = min(desktop_geom.size().height(), window_size.height())
        window_size = QSize(w, h)

        # and position it on the supplied desktop screen
        x = max(window_pos.x(), desktop_geom.left())
        y = max(window_pos.y(), desktop_geom.top())
        window_pos = QPoint(x, y)

        # set the geometry
        self.resize(window_size)
        self.move(window_pos)

        # restore window state
        if settings.has('MainWindow/state'):
            self.restoreState(settings.get('MainWindow/state'))
        else:
            self.setWindowState(Qt.WindowMaximized)

        # read in settings for children
        AlgorithmInputHistory().readSettings(settings)
        for widget in self.widgets:
            if hasattr(widget, 'readSettings'):
                widget.readSettings(settings)

    def writeSettings(self, settings):
        settings.set('MainWindow/size', self.size())  # QSize
        settings.set('MainWindow/position', self.pos())  # QPoint
        settings.set('MainWindow/state', self.saveState())  # QByteArray

        # write out settings for children
        AlgorithmInputHistory().writeSettings(settings)
        for widget in self.widgets:
            if hasattr(widget, 'writeSettings'):
                widget.writeSettings(settings)
Esempio n. 14
0
 def setUp(self):
     self.multifileinterpreter = mock.MagicMock()
     self.pr = ProjectRecovery(self.multifileinterpreter)
     self.working_directory = tempfile.mkdtemp()
Esempio n. 15
0
class MainWindow(QMainWindow):
    DOCKOPTIONS = QMainWindow.AllowTabbedDocks | QMainWindow.AllowNestedDocks

    def __init__(self):
        QMainWindow.__init__(self)

        # -- instance attributes --
        self.setWindowTitle(MAIN_WINDOW_TITLE)
        self.setObjectName(MAIN_WINDOW_OBJECT_NAME)

        # widgets
        self.messagedisplay = None
        self.ipythonconsole = None
        self.workspacewidget = None
        self.editor = None
        self.algorithm_selector = None
        self.plot_selector = None
        self.interface_manager = None
        self.script_repository = None
        self.widgets = []

        # Widget layout map: required for use in Qt.connection
        self._layout_widget_info = None

        # Menus
        self.file_menu = None
        self.file_menu_actions = None
        self.view_menu = None
        self.view_menu_actions = None
        self.view_menu_layouts = None
        self.interfaces_menu = None
        self.help_menu = None
        self.help_menu_actions = None

        # Allow splash screen text to be overridden in set_splash
        self.splash = SPLASH

        # Layout
        self.setDockOptions(self.DOCKOPTIONS)

        # Project
        self.project = None
        self.project_recovery = None

        # Interfaces
        self.interface_manager = None
        self.interface_executor = None
        self.interface_list = None

    def setup(self):
        # menus must be done first so they can be filled by the
        # plugins in register_plugin
        self.create_menus()

        # widgets
        # Log message display must be imported first
        self.set_splash("Loading message display")
        from workbench.plugins.logmessagedisplay import LogMessageDisplay
        self.messagedisplay = LogMessageDisplay(self)
        # this takes over stdout/stderr
        self.messagedisplay.register_plugin()
        self.widgets.append(self.messagedisplay)

        self.set_splash("Loading Algorithm Selector")
        from workbench.plugins.algorithmselectorwidget import AlgorithmSelector
        self.algorithm_selector = AlgorithmSelector(self)
        self.algorithm_selector.register_plugin()
        self.widgets.append(self.algorithm_selector)

        self.set_splash("Loading Plot Selector")
        from workbench.plugins.plotselectorwidget import PlotSelector
        self.plot_selector = PlotSelector(self)
        self.plot_selector.register_plugin()
        self.widgets.append(self.plot_selector)

        self.set_splash("Loading code editing widget")
        from workbench.plugins.editor import MultiFileEditor
        self.editor = MultiFileEditor(self)
        self.messagedisplay.display.setActiveScript(
            self.editor.editors.current_tab_filename)
        self.editor.register_plugin()
        self.widgets.append(self.editor)
        self.editor.editors.sig_code_exec_start.connect(
            self.messagedisplay.script_executing)
        self.editor.editors.sig_file_name_changed.connect(
            self.messagedisplay.file_name_modified)
        self.editor.editors.sig_current_tab_changed.connect(
            self.messagedisplay.current_tab_changed)

        self.set_splash("Loading IPython console")
        from workbench.plugins.jupyterconsole import JupyterConsole
        self.ipythonconsole = JupyterConsole(self)
        self.ipythonconsole.register_plugin()
        self.widgets.append(self.ipythonconsole)

        from workbench.plugins.workspacewidget import WorkspaceWidget
        self.workspacewidget = WorkspaceWidget(self)
        self.workspacewidget.register_plugin()
        prompt = CONF.get('project/prompt_on_deleting_workspace')
        self.workspacewidget.workspacewidget.enableDeletePrompt(bool(prompt))
        self.widgets.append(self.workspacewidget)

        # set the link between the algorithm and workspace widget
        self.algorithm_selector.algorithm_selector.set_get_selected_workspace_fn(
            self.workspacewidget.workspacewidget.getSelectedWorkspaceNames)

        # Set up the project, recovery and interface manager objects
        self.project = Project(GlobalFigureManager,
                               find_all_windows_that_are_savable)
        self.project_recovery = ProjectRecovery(
            globalfiguremanager=GlobalFigureManager,
            multifileinterpreter=self.editor.editors,
            main_window=self)

        self.interface_executor = PythonCodeExecution()
        self.interface_executor.sig_exec_error.connect(
            lambda errobj: logger.warning(str(errobj)))
        self.interface_manager = InterfaceManager()

        # uses default configuration as necessary
        self.setup_default_layouts()
        self.create_actions()
        self.readSettings(CONF)
        self.config_updated()

    def post_mantid_init(self):
        """Run any setup that requires mantid
        to have been initialized
        """
        self.redirect_python_warnings()
        self.populate_menus()
        self.algorithm_selector.refresh()

        # turn on algorithm factory notifications
        from mantid.api import AlgorithmFactory
        algorithm_factory = AlgorithmFactory.Instance()
        algorithm_factory.enableNotifications()

    def set_splash(self, msg=None):
        if not self.splash:
            return
        if msg:
            self.splash.showMessage(
                msg, Qt.AlignBottom | Qt.AlignLeft | Qt.AlignAbsolute,
                QColor(Qt.black))
        QApplication.processEvents(QEventLoop.AllEvents)

    def create_menus(self):
        self.file_menu = self.menuBar().addMenu("&File")
        self.view_menu = self.menuBar().addMenu("&View")
        self.interfaces_menu = self.menuBar().addMenu('&Interfaces')
        self.help_menu = self.menuBar().addMenu('&Help')

    def create_actions(self):
        # --- general application menu options --
        # file menu
        action_open = create_action(self,
                                    "Open Script",
                                    on_triggered=self.open_file,
                                    shortcut="Ctrl+O",
                                    shortcut_context=Qt.ApplicationShortcut)
        action_load_project = create_action(self,
                                            "Open Project",
                                            on_triggered=self.load_project)
        action_save_script = create_action(
            self,
            "Save Script",
            on_triggered=self.save_script,
            shortcut="Ctrl+S",
            shortcut_context=Qt.ApplicationShortcut)
        action_save_script_as = create_action(self,
                                              "Save Script as...",
                                              on_triggered=self.save_script_as)
        action_generate_ws_script = create_action(
            self,
            "Generate Recovery Script",
            on_triggered=self.generate_script_from_workspaces)
        action_save_project = create_action(self,
                                            "Save Project",
                                            on_triggered=self.save_project)
        action_save_project_as = create_action(
            self, "Save Project as...", on_triggered=self.save_project_as)
        action_manage_directories = create_action(
            self,
            "Manage User Directories",
            on_triggered=self.open_manage_directories)
        action_script_repository = create_action(
            self,
            "Script Repository",
            on_triggered=self.open_script_repository)
        action_settings = create_action(self,
                                        "Settings",
                                        on_triggered=self.open_settings_window)
        action_quit = create_action(self,
                                    "&Quit",
                                    on_triggered=self.close,
                                    shortcut="Ctrl+Q",
                                    shortcut_context=Qt.ApplicationShortcut)
        self.file_menu_actions = [
            action_open, action_load_project, None, action_save_script,
            action_save_script_as, action_generate_ws_script, None,
            action_save_project, action_save_project_as, None, action_settings,
            None, action_manage_directories, None, action_script_repository,
            None, action_quit
        ]

        # view menu
        action_restore_default = create_action(
            self,
            "Restore Default Layout",
            on_triggered=self.setup_default_layouts,
            shortcut="Shift+F10",
            shortcut_context=Qt.ApplicationShortcut)

        self.view_menu_layouts = self.view_menu.addMenu("&User Layouts")
        self.populate_layout_menu()

        self.view_menu_actions = [action_restore_default, None
                                  ] + self.create_widget_actions()

        # help menu
        action_mantid_help = create_action(
            self,
            "Mantid Help",
            on_triggered=self.open_mantid_help,
            shortcut='F1',
            shortcut_context=Qt.ApplicationShortcut)
        action_algorithm_descriptions = create_action(
            self,
            'Algorithm Descriptions',
            on_triggered=self.open_algorithm_descriptions_help)
        action_mantid_concepts = create_action(
            self,
            "Mantid Concepts",
            on_triggered=self.open_mantid_concepts_help)
        action_mantid_homepage = create_action(
            self, "Mantid Homepage", on_triggered=self.open_mantid_homepage)
        action_mantid_forum = create_action(
            self, "Mantid Forum", on_triggered=self.open_mantid_forum)
        action_about = create_action(self,
                                     "About Mantid Workbench",
                                     on_triggered=self.open_about)

        self.help_menu_actions = [
            action_mantid_help, action_mantid_concepts,
            action_algorithm_descriptions, None, action_mantid_homepage,
            action_mantid_forum, None, action_about
        ]

    def create_widget_actions(self):
        """
        Creates menu actions to show/hide dockable widgets.
        This uses all widgets that are in self.widgets
        :return: A list of show/hide actions for all widgets
        """
        widget_actions = []
        for widget in self.widgets:
            action = widget.dockwidget.toggleViewAction()
            widget_actions.append(action)
        return widget_actions

    def populate_menus(self):
        # Link to menus
        add_actions(self.file_menu, self.file_menu_actions)
        add_actions(self.view_menu, self.view_menu_actions)
        add_actions(self.help_menu, self.help_menu_actions)
        self.populate_interfaces_menu()

    def launch_custom_python_gui(self, filename):
        self.interface_executor.execute(open(filename).read(), filename)

    def launch_custom_cpp_gui(self, interface_name, submenu=None):
        """Create a new interface window if one does not already exist,
        else show existing window"""
        object_name = 'custom-cpp-interface-' + interface_name
        window = find_window(object_name, QMainWindow)
        if window is None:
            interface = self.interface_manager.createSubWindow(interface_name)
            interface.setObjectName(object_name)
            interface.setAttribute(Qt.WA_DeleteOnClose, True)
            # make indirect interfaces children of workbench
            if submenu == "Indirect":
                interface.setParent(self, interface.windowFlags())
            interface.show()
        else:
            if window.windowState() == Qt.WindowMinimized:
                window.setWindowState(Qt.WindowActive)
            else:
                window.raise_()

    def populate_interfaces_menu(self):
        """Populate then Interfaces menu with all Python and C++ interfaces"""
        self.interfaces_menu.clear()
        interface_dir = ConfigService['mantidqt.python_interfaces_directory']
        self.interface_list = self._discover_python_interfaces(interface_dir)
        self._discover_cpp_interfaces(self.interface_list)

        hidden_interfaces = ConfigService[
            'interfaces.categories.hidden'].split(';')

        keys = list(self.interface_list.keys())
        keys.sort()
        for key in keys:
            if key not in hidden_interfaces:
                submenu = self.interfaces_menu.addMenu(key)
                names = self.interface_list[key]
                names.sort()
                for name in names:
                    if '.py' in name:
                        action = submenu.addAction(
                            name.replace('.py', '').replace('_', ' '))
                        script = os.path.join(interface_dir, name)
                        action.triggered.connect(
                            lambda checked_py, script=script: self.
                            launch_custom_python_gui(script))
                    else:
                        action = submenu.addAction(name)
                        action.triggered.connect(
                            lambda checked_cpp, name=name, key=key: self.
                            launch_custom_cpp_gui(name, key))

    def redirect_python_warnings(self):
        """By default the warnings module writes warnings to sys.stderr. stderr is assumed to be
        an error channel so we don't confuse warnings with errors this redirects warnings
        from the warnings module to mantid.logger.warning
        """
        import warnings

        def to_mantid_warning(*args, **kwargs):
            logger.warning(warnings.formatwarning(*args, **kwargs))

        warnings.showwarning = to_mantid_warning

    def _discover_python_interfaces(self, interface_dir):
        """Return a dictionary mapping a category to a set of named Python interfaces"""
        items = ConfigService['mantidqt.python_interfaces'].split()
        # list of custom interfaces that are not qt4/qt5 compatible
        GUI_BLACKLIST = ['Frequency_Domain_Analysis_Old.py']

        # detect the python interfaces
        interfaces = {}
        for item in items:
            key, scriptname = item.split('/')
            if not os.path.exists(os.path.join(interface_dir, scriptname)):
                logger.warning('Failed to find script "{}" in "{}"'.format(
                    scriptname, interface_dir))
                continue
            if scriptname in GUI_BLACKLIST:
                logger.information('Not adding gui "{}"'.format(scriptname))
                continue
            interfaces.setdefault(key, []).append(scriptname)

        return interfaces

    def _discover_cpp_interfaces(self, interfaces):
        """Return a dictionary mapping a category to a set of named C++ interfaces"""
        cpp_interface_factory = UserSubWindowFactory.Instance()
        interface_names = cpp_interface_factory.keys()
        for name in interface_names:
            categories = cpp_interface_factory.categories(name)
            if len(categories) == 0:
                categories = ["General"]
            for category in categories:
                if category in interfaces.keys():
                    interfaces[category].append(name)
                else:
                    interfaces[category] = [name]

        return interfaces

    def add_dockwidget(self, plugin):
        """Create a dockwidget around a plugin and add the dock to window"""
        dockwidget, location = plugin.create_dockwidget()
        self.addDockWidget(location, dockwidget)

    # ----------------------- Layout ---------------------------------

    def populate_layout_menu(self):
        self.view_menu_layouts.clear()
        try:
            layout_dict = CONF.get("MainWindow/user_layouts")
        except KeyError:
            layout_dict = {}
        layout_keys = sorted(layout_dict.keys())
        layout_options = []
        for item in layout_keys:
            layout_options.append(
                self.create_load_layout_action(item, layout_dict[item]))
        layout_options.append(None)
        action_settings = create_action(
            self, "Settings", on_triggered=self.open_settings_layout_window)
        layout_options.append(action_settings)

        add_actions(self.view_menu_layouts, layout_options)

    def create_load_layout_action(self, layout_name, layout):
        action_load_layout = create_action(
            self, layout_name, on_triggered=lambda: self.restoreState(layout))
        return action_load_layout

    def prep_window_for_reset(self):
        """Function to reset all dock widgets to a state where they can be
        ordered by setup_default_layout"""
        for widget in self.widgets:
            widget.dockwidget.setFloating(
                False)  # Bring back any floating windows
            self.addDockWidget(Qt.LeftDockWidgetArea,
                               widget.dockwidget)  # Un-tabify all widgets
            widget.toggle_view(False)

    def setup_default_layouts(self):
        """Set the default layouts of the child widgets"""
        # layout definition
        logmessages = self.messagedisplay
        ipython = self.ipythonconsole
        workspacewidget = self.workspacewidget
        editor = self.editor
        algorithm_selector = self.algorithm_selector
        plot_selector = self.plot_selector
        default_layout = {
            'widgets': [
                # column 0
                [[workspacewidget], [algorithm_selector, plot_selector]],
                # column 1
                [[editor, ipython]],
                # column 2
                [[logmessages]]
            ],
            'width-fraction': [
                0.25,  # column 0 width
                0.50,  # column 1 width
                0.25
            ],  # column 2 width
            'height-fraction': [
                [0.5, 0.5],  # column 0 row heights
                [1.0],  # column 1 row heights
                [1.0]
            ]  # column 2 row heights
        }

        size = self.size()  # Preserve size on reset
        self.arrange_layout(default_layout)
        self.resize(size)

    def arrange_layout(self, layout):
        """Arrange the layout of the child widgets according to the supplied layout"""
        self.prep_window_for_reset()
        widgets_layout = layout['widgets']
        with widget_updates_disabled(self):
            # flatten list
            widgets = [
                item for column in widgets_layout for row in column
                for item in row
            ]
            # show everything
            for w in widgets:
                w.toggle_view(True)
            # split everything on the horizontal
            for i in range(len(widgets) - 1):
                first, second = widgets[i], widgets[i + 1]
                self.splitDockWidget(first.dockwidget, second.dockwidget,
                                     Qt.Horizontal)
            # now arrange the rows
            for column in widgets_layout:
                for i in range(len(column) - 1):
                    first_row, second_row = column[i], column[i + 1]
                    self.splitDockWidget(first_row[0].dockwidget,
                                         second_row[0].dockwidget, Qt.Vertical)
            # and finally tabify those in the same position
            for column in widgets_layout:
                for row in column:
                    for i in range(len(row) - 1):
                        first, second = row[i], row[i + 1]
                        self.tabifyDockWidget(first.dockwidget,
                                              second.dockwidget)

                    # Raise front widget per row
                    row[0].dockwidget.show()
                    row[0].dockwidget.raise_()

    # ----------------------- Events ---------------------------------
    def closeEvent(self, event):
        if self.project.is_saving or self.project.is_loading:
            event.ignore()
            self.project.inform_user_not_possible()
            return

        # Check whether or not to save project
        if not self.project.saved:
            # Offer save
            if self.project.offer_save(self):
                # Cancel has been clicked
                event.ignore()
                return

        # Close editors
        if self.editor.app_closing():
            # write out any changes to the mantid config file
            ConfigService.saveConfig(ConfigService.getUserFilename())
            # write current window information to global settings object
            self.writeSettings(CONF)
            # Close all open plots
            # We don't want this at module scope here
            import matplotlib.pyplot as plt  # noqa
            plt.close('all')

            app = QApplication.instance()
            if app is not None:
                app.closeAllWindows()

            # Kill the project recovery thread and don't restart should a save be in progress and clear out current
            # recovery checkpoint as it is closing properly
            self.project_recovery.stop_recovery_thread()
            self.project_recovery.closing_workbench = True
            self.project_recovery.remove_current_pid_folder()

            self.interface_manager.closeHelpWindow()

            event.accept()
        else:
            # Cancel was pressed when closing an editor
            event.ignore()

    # ----------------------- Slots ---------------------------------
    def open_file(self):
        # todo: when more file types are added this should
        # live in its own type
        filepath, _ = QFileDialog.getOpenFileName(self, "Open File...", "",
                                                  "Python (*.py)")
        if not filepath:
            return
        self.editor.open_file_in_new_tab(filepath)

    def save_script(self):
        self.editor.save_current_file()

    def save_script_as(self):
        self.editor.save_current_file_as()

    def generate_script_from_workspaces(self):
        task = BlockingAsyncTaskWithCallback(
            target=self._generate_script_from_workspaces,
            blocking_cb=QApplication.processEvents)
        task.start()

    def _generate_script_from_workspaces(self):
        script = "from mantid.simpleapi import *\n\n" + get_all_workspace_history_from_ads(
        )
        QAppThreadCall(self.editor.open_script_in_new_tab)(script)

    def save_project(self):
        self.project.save()

    def save_project_as(self):
        self.project.save_as()

    def load_project(self):
        self.project.load()

    def open_manage_directories(self):
        manageuserdirectories.ManageUserDirectories.openManageUserDirectories()

    def open_script_repository(self):
        self.script_repository = ScriptRepositoryView(self)
        self.script_repository.loadScript.connect(
            self.editor.open_file_in_new_tab)
        self.script_repository.setAttribute(Qt.WA_DeleteOnClose, True)
        self.script_repository.show()

    def open_settings_window(self):
        settings = SettingsPresenter(self)
        settings.show()

    def open_settings_layout_window(self):
        settings = SettingsPresenter(self)
        settings.show()
        settings.general_settings.focus_layout_box()

    def config_updated(self):
        """
        Updates the widgets that depend on settings from the Workbench Config.
        """
        self.editor.load_settings_from_config(CONF)
        self.project.load_settings_from_config(CONF)
        self.algorithm_selector.refresh()
        self.populate_interfaces_menu()
        self.workspacewidget.refresh_workspaces()

    def open_algorithm_descriptions_help(self):
        self.interface_manager.showAlgorithmHelp('')

    def open_mantid_concepts_help(self):
        self.interface_manager.showConceptHelp('')

    def open_mantid_help(self):
        self.interface_manager.showHelpPage('')

    def open_mantid_homepage(self):
        self.interface_manager.showWebPage('https://www.mantidproject.org')

    def open_mantid_forum(self):
        self.interface_manager.showWebPage('https://forum.mantidproject.org/')

    def open_about(self):
        about = AboutPresenter(self)
        about.show()

    def readSettings(self, settings):
        qapp = QApplication.instance()
        qapp.setAttribute(Qt.AA_UseHighDpiPixmaps)
        if hasattr(Qt, 'AA_EnableHighDpiScaling'):
            qapp.setAttribute(Qt.AA_EnableHighDpiScaling,
                              settings.get('high_dpi_scaling'))

        # get the saved window geometry
        window_size = settings.get('MainWindow/size')
        if not isinstance(window_size, QSize):
            window_size = QSize(*window_size)
        window_pos = settings.get('MainWindow/position')
        if not isinstance(window_pos, QPoint):
            window_pos = QPoint(*window_pos)
        if settings.has('MainWindow/font'):
            font_string = settings.get('MainWindow/font').split(',')
            font = QFontDatabase().font(font_string[0], font_string[-1],
                                        int(font_string[1]))
            qapp.setFont(font)

        # make sure main window is smaller than the desktop
        desktop = QDesktopWidget()

        # this gives the maximum screen number if the position is off screen
        screen = desktop.screenNumber(window_pos)

        # recalculate the window size
        desktop_geom = desktop.screenGeometry(screen)
        w = min(desktop_geom.size().width(), window_size.width())
        h = min(desktop_geom.size().height(), window_size.height())
        window_size = QSize(w, h)

        # and position it on the supplied desktop screen
        x = max(window_pos.x(), desktop_geom.left())
        y = max(window_pos.y(), desktop_geom.top())
        window_pos = QPoint(x, y)

        # set the geometry
        self.resize(window_size)
        self.move(window_pos)

        # restore window state
        if settings.has('MainWindow/state'):
            self.restoreState(settings.get('MainWindow/state'))
        else:
            self.setWindowState(Qt.WindowMaximized)

        # read in settings for children
        AlgorithmInputHistory().readSettings(settings)
        for widget in self.widgets:
            if hasattr(widget, 'readSettings'):
                widget.readSettings(settings)

    def writeSettings(self, settings):
        settings.set('MainWindow/size', self.size())  # QSize
        settings.set('MainWindow/position', self.pos())  # QPoint
        settings.set('MainWindow/state', self.saveState())  # QByteArray

        # write out settings for children
        AlgorithmInputHistory().writeSettings(settings)
        for widget in self.widgets:
            if hasattr(widget, 'writeSettings'):
                widget.writeSettings(settings)
    def setUp(self):
        self.working_directory = tempfile.mkdtemp()

        self.pr = ProjectRecovery(None)
        self.pr_saver = self.pr.saver
Esempio n. 17
0
    def setup(self):
        # menus must be done first so they can be filled by the
        # plugins in register_plugin
        self.create_menus()

        # widgets
        # Log message display must be imported first
        self.set_splash("Loading message display")
        from workbench.plugins.logmessagedisplay import LogMessageDisplay
        self.messagedisplay = LogMessageDisplay(self)
        # this takes over stdout/stderr
        self.messagedisplay.register_plugin()
        self.widgets.append(self.messagedisplay)

        self.set_splash("Loading Algorithm Selector")
        from workbench.plugins.algorithmselectorwidget import AlgorithmSelector
        self.algorithm_selector = AlgorithmSelector(self)
        self.algorithm_selector.register_plugin()
        self.widgets.append(self.algorithm_selector)

        self.set_splash("Loading Plot Selector")
        from workbench.plugins.plotselectorwidget import PlotSelector
        self.plot_selector = PlotSelector(self)
        self.plot_selector.register_plugin()
        self.widgets.append(self.plot_selector)

        self.set_splash("Loading code editing widget")
        from workbench.plugins.editor import MultiFileEditor
        self.editor = MultiFileEditor(self)
        self.messagedisplay.display.setActiveScript(
            self.editor.editors.current_tab_filename)
        self.editor.register_plugin()
        self.widgets.append(self.editor)
        self.editor.editors.sig_code_exec_start.connect(
            self.messagedisplay.script_executing)
        self.editor.editors.sig_file_name_changed.connect(
            self.messagedisplay.file_name_modified)
        self.editor.editors.sig_current_tab_changed.connect(
            self.messagedisplay.current_tab_changed)

        self.set_splash("Loading IPython console")
        from workbench.plugins.jupyterconsole import JupyterConsole
        self.ipythonconsole = JupyterConsole(self)
        self.ipythonconsole.register_plugin()
        self.widgets.append(self.ipythonconsole)

        from workbench.plugins.workspacewidget import WorkspaceWidget
        self.workspacewidget = WorkspaceWidget(self)
        self.workspacewidget.register_plugin()
        prompt = CONF.get('project/prompt_on_deleting_workspace')
        self.workspacewidget.workspacewidget.enableDeletePrompt(bool(prompt))
        self.widgets.append(self.workspacewidget)

        # set the link between the algorithm and workspace widget
        self.algorithm_selector.algorithm_selector.set_get_selected_workspace_fn(
            self.workspacewidget.workspacewidget.getSelectedWorkspaceNames)

        # Set up the project, recovery and interface manager objects
        self.project = Project(GlobalFigureManager,
                               find_all_windows_that_are_savable)
        self.project_recovery = ProjectRecovery(
            globalfiguremanager=GlobalFigureManager,
            multifileinterpreter=self.editor.editors,
            main_window=self)

        self.interface_executor = PythonCodeExecution()
        self.interface_executor.sig_exec_error.connect(
            lambda errobj: logger.warning(str(errobj)))
        self.interface_manager = InterfaceManager()

        # uses default configuration as necessary
        self.setup_default_layouts()
        self.create_actions()
        self.readSettings(CONF)
        self.config_updated()
Esempio n. 18
0
class MainWindow(QMainWindow):
    DOCKOPTIONS = QMainWindow.AllowTabbedDocks | QMainWindow.AllowNestedDocks

    def __init__(self):
        QMainWindow.__init__(self)

        # -- instance attributes --
        self.setWindowTitle(MAIN_WINDOW_TITLE)
        self.setObjectName(MAIN_WINDOW_OBJECT_NAME)

        # widgets
        self.memorywidget = None
        self.messagedisplay = None
        self.ipythonconsole = None
        self.workspacewidget = None
        self.workspacecalculator = None
        self.editor = None
        self.algorithm_selector = None
        self.plot_selector = None
        self.interface_manager = None
        self.script_repository = None
        self.widgets = []

        # Widget layout map: required for use in Qt.connection
        self._layout_widget_info = None

        # Menus
        self.file_menu = None
        self.file_menu_actions = None
        self.view_menu = None
        self.view_menu_actions = None
        self.view_menu_layouts = None
        self.interfaces_menu = None
        self.help_menu = None
        self.help_menu_actions = None

        # Allow splash screen text to be overridden in set_splash
        self.splash = SPLASH

        # Layout
        self.setDockOptions(self.DOCKOPTIONS)

        # Project
        self.project = None
        self.project_recovery = None

        # Interfaces
        self.interface_manager = None
        self.interface_executor = None
        self.interface_list = None

        self.could_restore_state = False

    def setup(self):
        # menus must be done first so they can be filled by the
        # plugins in register_plugin
        self.create_menus()

        # widgets
        # Log message display must be imported first
        self.set_splash("Loading message display")
        from workbench.plugins.logmessagedisplay import LogMessageDisplay
        self.messagedisplay = LogMessageDisplay(self)
        # this takes over stdout/stderr
        self.messagedisplay.register_plugin()
        # read settings early so that logging level is in place before framework mgr created
        self.messagedisplay.readSettings(CONF)
        self.widgets.append(self.messagedisplay)

        self.set_splash("Loading Algorithm Selector")
        from workbench.plugins.algorithmselectorwidget import AlgorithmSelector
        self.algorithm_selector = AlgorithmSelector(self)
        self.algorithm_selector.register_plugin()
        self.widgets.append(self.algorithm_selector)

        self.set_splash("Loading Plot Selector")
        from workbench.plugins.plotselectorwidget import PlotSelector
        self.plot_selector = PlotSelector(self)
        self.plot_selector.register_plugin()
        self.widgets.append(self.plot_selector)

        self.set_splash("Loading code editing widget")
        from workbench.plugins.editor import MultiFileEditor
        self.editor = MultiFileEditor(self)
        self.messagedisplay.display.setActiveScript(
            self.editor.editors.current_tab_filename)
        self.editor.register_plugin()
        self.widgets.append(self.editor)
        self.editor.editors.sig_code_exec_start.connect(
            self.messagedisplay.script_executing)
        self.editor.editors.sig_file_name_changed.connect(
            self.messagedisplay.file_name_modified)
        self.editor.editors.sig_current_tab_changed.connect(
            self.messagedisplay.current_tab_changed)

        self.set_splash("Loading IPython console")
        from workbench.plugins.jupyterconsole import JupyterConsole
        self.ipythonconsole = JupyterConsole(self)
        self.ipythonconsole.register_plugin()
        self.widgets.append(self.ipythonconsole)

        from workbench.plugins.workspacewidget import WorkspaceWidget
        self.workspacewidget = WorkspaceWidget(self)
        self.workspacewidget.register_plugin()
        prompt = CONF.get('project/prompt_on_deleting_workspace')
        self.workspacewidget.workspacewidget.enableDeletePrompt(bool(prompt))
        self.widgets.append(self.workspacewidget)

        self.set_splash("Loading memory widget")
        from workbench.plugins.memorywidget import MemoryWidget
        self.memorywidget = MemoryWidget(self)
        self.memorywidget.register_plugin()
        self.widgets.append(self.memorywidget)

        # set the link between the algorithm and workspace widget
        self.algorithm_selector.algorithm_selector.set_get_selected_workspace_fn(
            self.workspacewidget.workspacewidget.getSelectedWorkspaceNames)

        from workbench.plugins.workspacecalculatorwidget import WorkspaceCalculatorWidget
        self.workspacecalculator = WorkspaceCalculatorWidget(self)
        self.workspacecalculator.register_plugin()
        self.widgets.append(self.workspacecalculator)

        # Set up the project, recovery and interface manager objects
        self.project = Project(GlobalFigureManager,
                               find_all_windows_that_are_savable)
        self.project_recovery = ProjectRecovery(
            globalfiguremanager=GlobalFigureManager,
            multifileinterpreter=self.editor.editors,
            main_window=self)

        self.interface_executor = PythonCodeExecution()
        self.interface_executor.sig_exec_error.connect(
            lambda errobj: logger.warning(str(errobj)))
        self.interface_manager = InterfaceManager()

        # uses default configuration as necessary
        self.setup_default_layouts()
        self.create_actions()
        self.readSettings(CONF)
        self.config_updated()

        self.override_python_input()

        # Ensure windows created after the main window have their own menu bars (on mac)
        QCoreApplication.setAttribute(Qt.AA_DontUseNativeMenuBar, True)

    def post_mantid_init(self):
        """Run any setup that requires mantid
        to have been initialized
        """
        self.redirect_python_warnings()
        self.populate_menus()
        self.algorithm_selector.refresh()

        # turn on algorithm factory notifications
        from mantid.api import AlgorithmFactory
        algorithm_factory = AlgorithmFactory.Instance()
        algorithm_factory.enableNotifications()

    def set_splash(self, msg=None):
        if not self.splash:
            return
        if msg:
            self.splash.showMessage(
                msg, int(Qt.AlignBottom | Qt.AlignLeft | Qt.AlignAbsolute),
                QColor(Qt.black))
        QApplication.processEvents(QEventLoop.AllEvents)

    def create_menus(self):
        self.file_menu = self.menuBar().addMenu("&File")
        self.view_menu = self.menuBar().addMenu("&View")
        self.interfaces_menu = self.menuBar().addMenu('&Interfaces')
        self.help_menu = self.menuBar().addMenu('&Help')

    def create_actions(self):
        # --- general application menu options --
        # file menu
        action_open = create_action(self,
                                    "Open Script",
                                    on_triggered=self.open_file,
                                    shortcut="Ctrl+O")
        action_load_project = create_action(self,
                                            "Open Project",
                                            on_triggered=self.load_project)
        action_save_script = create_action(self,
                                           "Save Script",
                                           on_triggered=self.save_script,
                                           shortcut="Ctrl+S")
        action_save_script_as = create_action(self,
                                              "Save Script as...",
                                              on_triggered=self.save_script_as)
        action_generate_ws_script = create_action(
            self,
            "Generate Recovery Script",
            on_triggered=self.generate_script_from_workspaces)
        action_save_project = create_action(self,
                                            "Save Project",
                                            on_triggered=self.save_project)
        action_save_project_as = create_action(
            self, "Save Project as...", on_triggered=self.save_project_as)
        action_manage_directories = create_action(
            self,
            "Manage User Directories",
            on_triggered=self.open_manage_directories)
        action_script_repository = create_action(
            self,
            "Script Repository",
            on_triggered=self.open_script_repository)
        action_settings = create_action(self,
                                        "Settings",
                                        on_triggered=self.open_settings_window)
        action_quit = create_action(self,
                                    "&Quit",
                                    on_triggered=self.close,
                                    shortcut="Ctrl+Q")
        action_clear_all_memory = create_action(
            self,
            "Clear All Memory",
            on_triggered=self.clear_all_memory_action,
            shortcut="Ctrl+Shift+L")

        menu_recently_closed_scripts = RecentlyClosedScriptsMenu(self)
        self.editor.editors.sig_tab_closed.connect(
            menu_recently_closed_scripts.add_script_to_settings)

        self.file_menu_actions = [
            action_open, action_load_project, None, action_save_script,
            action_save_script_as, menu_recently_closed_scripts,
            action_generate_ws_script, None, action_save_project,
            action_save_project_as, None, action_settings, None,
            action_manage_directories, None, action_script_repository, None,
            action_clear_all_memory, None, action_quit
        ]

        # view menu
        action_restore_default = create_action(
            self,
            "Restore Default Layout",
            on_triggered=self.setup_default_layouts,
            shortcut="Shift+F10",
            shortcut_context=Qt.ApplicationShortcut)

        self.view_menu_layouts = self.view_menu.addMenu("&User Layouts")
        self.populate_layout_menu()

        self.view_menu_actions = [action_restore_default, None
                                  ] + self.create_widget_actions()

        # help menu
        action_mantid_help = create_action(
            self,
            "Mantid Help",
            on_triggered=self.open_mantid_help,
            shortcut='F1',
            shortcut_context=Qt.ApplicationShortcut)
        action_algorithm_descriptions = create_action(
            self,
            'Algorithm Descriptions',
            on_triggered=self.open_algorithm_descriptions_help)
        action_mantid_concepts = create_action(
            self,
            "Mantid Concepts",
            on_triggered=self.open_mantid_concepts_help)
        action_mantid_homepage = create_action(
            self, "Mantid Homepage", on_triggered=self.open_mantid_homepage)
        action_mantid_forum = create_action(
            self, "Mantid Forum", on_triggered=self.open_mantid_forum)
        action_about = create_action(self,
                                     "About Mantid Workbench",
                                     on_triggered=self.open_about)

        self.help_menu_actions = [
            action_mantid_help, action_mantid_concepts,
            action_algorithm_descriptions, None, action_mantid_homepage,
            action_mantid_forum, None, action_about
        ]

    def create_widget_actions(self):
        """
        Creates menu actions to show/hide dockable widgets.
        This uses all widgets that are in self.widgets
        :return: A list of show/hide actions for all widgets
        """
        widget_actions = []
        for widget in self.widgets:
            action = widget.dockwidget.toggleViewAction()
            widget_actions.append(action)
        return widget_actions

    def populate_menus(self):
        # Link to menus
        add_actions(self.file_menu, self.file_menu_actions)
        add_actions(self.view_menu, self.view_menu_actions)
        add_actions(self.help_menu, self.help_menu_actions)

    def launch_custom_python_gui(self, filename):
        self.interface_executor.execute(open(filename).read(), filename)

    def launch_custom_cpp_gui(self, interface_name, submenu=None):
        """Create a new interface window if one does not already exist,
        else show existing window"""
        object_name = 'custom-cpp-interface-' + interface_name
        window = find_window(object_name, QMainWindow)
        if window is None:
            interface = self.interface_manager.createSubWindow(interface_name)
            interface.setObjectName(object_name)
            interface.setAttribute(Qt.WA_DeleteOnClose, True)
            parent, flags = get_window_config()
            if parent:
                interface.setParent(parent, flags)
            interface.show()
        else:
            if window.windowState() == Qt.WindowMinimized:
                window.setWindowState(Qt.WindowActive)
            else:
                window.raise_()

    def populate_interfaces_menu(self):
        """Populate then Interfaces menu with all Python and C++ interfaces"""
        self.interfaces_menu.clear()
        interface_dir = _get_interface_dir()

        self.interface_list, registers_to_run = self._discover_python_interfaces(
            interface_dir)
        self._discover_cpp_interfaces(self.interface_list)
        hidden_interfaces = ConfigService[
            'interfaces.categories.hidden'].split(';')

        keys = list(self.interface_list.keys())
        keys.sort()
        for key in keys:
            if key not in hidden_interfaces:
                submenu = self.interfaces_menu.addMenu(key)
                names = self.interface_list[key]
                names.sort()
                for name in names:
                    if '.py' in name:
                        action = submenu.addAction(
                            name.replace('.py', '').replace('_', ' '))
                        script = os.path.join(interface_dir, name)
                        action.triggered.connect(
                            lambda checked_py, script=script: self.
                            launch_custom_python_gui(script))
                    else:
                        action = submenu.addAction(name)
                        action.triggered.connect(
                            lambda checked_cpp, name=name, key=key: self.
                            launch_custom_cpp_gui(name, key))
        # these register scripts contain code to register encoders and decoders to work with project save before the
        # corresponding interface has been initialised. This is a temporary measure pending harmonisation of cpp/python
        # interfaces
        for reg_list in registers_to_run.values():
            for register in reg_list:
                file_path = os.path.join(interface_dir, register)
                with open(file_path) as handle:
                    self.interface_executor.execute(handle.read(), file_path)

    def redirect_python_warnings(self):
        """By default the warnings module writes warnings to sys.stderr. stderr is assumed to be
        an error channel so we don't confuse warnings with errors this redirects warnings
        from the warnings module to mantid.logger.warning
        """
        import warnings

        def to_mantid_warning(*args, **kwargs):
            logger.warning(warnings.formatwarning(*args, **kwargs))

        warnings.showwarning = to_mantid_warning

    def _discover_python_interfaces(self, interface_dir):
        """Return a dictionary mapping a category to a set of named Python interfaces"""
        items = ConfigService['mantidqt.python_interfaces'].split()
        try:
            register_items = ConfigService[
                'mantidqt.python_interfaces_io_registry'].split()
        except KeyError:
            register_items = []
        # detect the python interfaces
        interfaces = {}
        registers_to_run = {}
        for item in items:
            key, scriptname = item.split('/')
            reg_name = scriptname[:-3] + '_register.py'
            if reg_name in register_items and os.path.exists(
                    os.path.join(interface_dir, reg_name)):
                registers_to_run.setdefault(key, []).append(reg_name)
            if not os.path.exists(os.path.join(interface_dir, scriptname)):
                logger.warning('Failed to find script "{}" in "{}"'.format(
                    scriptname, interface_dir))
                continue
            interfaces.setdefault(key, []).append(scriptname)

        return interfaces, registers_to_run

    def _discover_cpp_interfaces(self, interfaces):
        """Return a dictionary mapping a category to a set of named C++ interfaces"""
        cpp_interface_factory = UserSubWindowFactory.Instance()
        interface_names = cpp_interface_factory.keys()
        for name in interface_names:
            categories = cpp_interface_factory.categories(name)
            if len(categories) == 0:
                categories = ["General"]
            for category in categories:
                if category in interfaces.keys():
                    interfaces[category].append(name)
                else:
                    interfaces[category] = [name]

        return interfaces

    def add_dockwidget(self, plugin):
        """Create a dockwidget around a plugin and add the dock to window"""
        dockwidget, location = plugin.create_dockwidget()
        self.addDockWidget(location, dockwidget)

    # ----------------------- Layout ---------------------------------

    def populate_layout_menu(self):
        self.view_menu_layouts.clear()
        try:
            layout_dict = CONF.get("MainWindow/user_layouts")
        except KeyError:
            layout_dict = {}
        layout_keys = sorted(layout_dict.keys())
        layout_options = []
        for item in layout_keys:
            layout_options.append(
                self.create_load_layout_action(item, layout_dict[item]))
        layout_options.append(None)
        action_settings = create_action(
            self, "Settings", on_triggered=self.open_settings_layout_window)
        layout_options.append(action_settings)

        add_actions(self.view_menu_layouts, layout_options)

    def create_load_layout_action(self, layout_name, layout):
        action_load_layout = create_action(
            self,
            layout_name,
            on_triggered=lambda: self.attempt_to_restore_state(layout))
        return action_load_layout

    def prep_window_for_reset(self):
        """Function to reset all dock widgets to a state where they can be
        ordered by setup_default_layout"""
        for widget in self.widgets:
            widget.dockwidget.setFloating(
                False)  # Bring back any floating windows
            self.addDockWidget(Qt.LeftDockWidgetArea,
                               widget.dockwidget)  # Un-tabify all widgets
            widget.toggle_view(False)

    def setup_default_layouts(self):
        """Set the default layouts of the child widgets"""
        # layout definition
        memorywidget = self.memorywidget
        logmessages = self.messagedisplay
        ipython = self.ipythonconsole
        workspacewidget = self.workspacewidget
        editor = self.editor
        algorithm_selector = self.algorithm_selector
        plot_selector = self.plot_selector
        workspacecalculator = self.workspacecalculator
        # If more than two rows are needed in a column,
        # arrange_layout function needs to be revisited.
        # In the first column, there are three widgets in two rows
        # as the algorithm_selector and plot_selector are tabified.
        default_layout = {
            'widgets': [
                # column 0
                [[workspacewidget], [algorithm_selector, plot_selector]],
                # column 1
                [[editor, ipython], [workspacecalculator]],
                # column 2
                [[memorywidget], [logmessages]]
            ],
        }

        size = self.size()  # Preserve size on reset
        self.arrange_layout(default_layout)
        self.resize(size)

    def arrange_layout(self, layout):
        """Arrange the layout of the child widgets according to the supplied layout"""
        self.prep_window_for_reset()
        widgets_layout = layout['widgets']
        with widget_updates_disabled(self):
            # flatten list
            widgets = [
                item for column in widgets_layout for row in column
                for item in row
            ]
            # show everything
            for w in widgets:
                w.toggle_view(True)
            # split everything on the horizontal
            for i in range(len(widgets) - 1):
                first, second = widgets[i], widgets[i + 1]
                self.splitDockWidget(first.dockwidget, second.dockwidget,
                                     Qt.Horizontal)
            # now arrange the rows
            for column in widgets_layout:
                for i in range(len(column) - 1):
                    first_row, second_row = column[i], column[i + 1]
                    self.splitDockWidget(first_row[0].dockwidget,
                                         second_row[0].dockwidget, Qt.Vertical)

            # and finally tabify those in the same position
            for column in widgets_layout:
                for row in column:
                    for i in range(len(row) - 1):
                        first, second = row[i], row[i + 1]
                        self.tabifyDockWidget(first.dockwidget,
                                              second.dockwidget)

                    # Raise front widget per row
                    row[0].dockwidget.show()
                    row[0].dockwidget.raise_()

    # ----------------------- Events ---------------------------------
    def closeEvent(self, event):
        if self.project is not None:
            if self.project.is_saving or self.project.is_loading:
                event.ignore()
                self.project.inform_user_not_possible()
                return

            # Check whether or not to save project
            if not self.project.saved:
                # Offer save
                if self.project.offer_save(self):
                    # Cancel has been clicked
                    event.ignore()
                    return

        # Close editors
        if self.editor is None or self.editor.app_closing():
            # write out any changes to the mantid config file
            ConfigService.saveConfig(ConfigService.getUserFilename())
            # write current window information to global settings object
            self.writeSettings(CONF)
            # Close all open plots
            # We don't want this at module scope here
            import matplotlib.pyplot as plt  # noqa
            plt.close('all')

            # Cancel all running (managed) algorithms
            AlgorithmManager.Instance().cancelAll()

            app = QApplication.instance()
            if app is not None:
                app.closeAllWindows()

            # Kill the project recovery thread and don't restart should a save be in progress and clear out current
            # recovery checkpoint as it is closing properly
            if self.project_recovery is not None:
                self.project_recovery.stop_recovery_thread()
                self.project_recovery.closing_workbench = True

            # Cancel memory widget thread
            if self.memorywidget is not None:
                self.memorywidget.presenter.cancel_memory_update()

            if self.interface_manager is not None:
                self.interface_manager.closeHelpWindow()

            if self.workspacecalculator is not None:
                self.workspacecalculator.view.closeEvent(event)

            if self.project_recovery is not None:
                # Do not merge this block with the above block that
                # starts with the same check.
                # We deliberately split the call to stop the recovery
                # thread and removal of the checkpoints folder to
                # allow for the maximum amount of time for the recovery
                # thread to finish. Any errors here are ignored as exceptions
                # on shutdown cannot be handled in a meaningful way.
                # Future runs of project recovery will clean any stale points
                # after a month
                self.project_recovery.remove_current_pid_folder(
                    ignore_errors=True)

            event.accept()
        else:
            # Cancel was pressed when closing an editor
            event.ignore()

    # ----------------------- Slots ---------------------------------
    def open_file(self):
        # todo: when more file types are added this should
        # live in its own type
        defaultSaveDirectory = ConfigService['defaultsave.directory']
        filepath, _ = QFileDialog.getOpenFileName(self, "Open File...",
                                                  defaultSaveDirectory,
                                                  "Python (*.py)")
        if not filepath:
            return
        self.editor.open_file_in_new_tab(filepath)

    def save_script(self):
        self.editor.save_current_file()

    def save_script_as(self):
        self.editor.save_current_file_as()

    def generate_script_from_workspaces(self):
        if not self.workspacewidget.empty_of_workspaces():
            task = BlockingAsyncTaskWithCallback(
                target=self._generate_script_from_workspaces,
                blocking_cb=QApplication.processEvents)
            task.start()
        else:
            # Tell users they need a workspace to do that
            QMessageBox().warning(
                None, "No Workspaces!",
                "In order to generate a recovery script there needs to be some workspaces.",
                QMessageBox.Ok)

    def _generate_script_from_workspaces(self):
        script = "from mantid.simpleapi import *\n\n" + get_all_workspace_history_from_ads(
        )
        QAppThreadCall(self.editor.open_script_in_new_tab)(script)

    def save_project(self):
        self.project.save(CONF)

    def save_project_as(self):
        self.project.open_project_save_dialog(CONF)

    def load_project(self):
        self.project.load()

    def open_manage_directories(self):
        manageuserdirectories.ManageUserDirectories.openManageUserDirectories()

    def open_script_repository(self):
        self.script_repository = ScriptRepositoryView(self)
        self.script_repository.loadScript.connect(
            self.editor.open_file_in_new_tab)
        self.script_repository.setAttribute(Qt.WA_DeleteOnClose, True)
        self.script_repository.show()

    def open_settings_window(self):
        settings = SettingsPresenter(self)
        settings.show()

    def open_settings_layout_window(self):
        settings = SettingsPresenter(self)
        settings.show()
        settings.general_settings.focus_layout_box()

    def clear_all_memory_action(self):
        """
        Creates Question QMessageBox to check user wants to clear all memory
        when action is pressed from file menu
        """
        msg = QMessageBox(
            QMessageBox.Question, "Clear All",
            "All workspaces and windows will be removed.\nAre you sure?")
        msg.addButton(QMessageBox.Ok)
        msg.addButton(QMessageBox.Cancel)
        msg.setWindowIcon(QIcon(':/images/MantidIcon.ico'))
        reply = msg.exec()
        if reply == QMessageBox.Ok:
            self.clear_all_memory()

    def clear_all_memory(self):
        """
        Wrapper for call to FrameworkManager to clear all memory
        """
        FrameworkManager.Instance().clear()

    def config_updated(self):
        """
        Updates the widgets that depend on settings from the Workbench Config.
        """
        self.editor.load_settings_from_config(CONF)
        self.project.load_settings_from_config(CONF)
        self.algorithm_selector.refresh()
        self.populate_interfaces_menu()
        self.workspacewidget.refresh_workspaces()

    def open_algorithm_descriptions_help(self):
        self.interface_manager.showAlgorithmHelp('')

    def open_mantid_concepts_help(self):
        self.interface_manager.showConceptHelp('')

    def open_mantid_help(self):
        UsageService.registerFeatureUsage(FeatureType.Feature.Interface,
                                          ["Mantid Help"], False)
        self.interface_manager.showHelpPage('')

    def open_mantid_homepage(self):
        self.interface_manager.showWebPage('https://www.mantidproject.org')

    def open_mantid_forum(self):
        self.interface_manager.showWebPage('https://forum.mantidproject.org/')

    def open_about(self):
        about = AboutPresenter(self)
        about.show()

    def readSettings(self, settings):
        qapp = QApplication.instance()

        # get the saved window geometry
        window_size = settings.get('MainWindow/size')
        if not isinstance(window_size, QSize):
            window_size = QSize(*window_size)
        window_pos = settings.get('MainWindow/position')
        if not isinstance(window_pos, QPoint):
            window_pos = QPoint(*window_pos)
        if settings.has('MainWindow/font'):
            font_string = settings.get('MainWindow/font').split(',')
            font = QFontDatabase().font(font_string[0], font_string[-1],
                                        int(font_string[1]))
            qapp.setFont(font)

        # reset font for ipython console to ensure it stays monospace
        self.ipythonconsole.console.reset_font()

        # make sure main window is smaller than the desktop
        desktop = QDesktopWidget()

        # this gives the maximum screen number if the position is off screen
        screen = desktop.screenNumber(window_pos)

        # recalculate the window size
        desktop_geom = desktop.availableGeometry(screen)
        w = min(desktop_geom.size().width(), window_size.width())
        h = min(desktop_geom.size().height(), window_size.height())
        window_size = QSize(w, h)

        # and position it on the supplied desktop screen
        x = max(window_pos.x(), desktop_geom.left())
        y = max(window_pos.y(), desktop_geom.top())
        if x + w > desktop_geom.right():
            x = desktop_geom.right() - w
        if y + h > desktop_geom.bottom():
            y = desktop_geom.bottom() - h
        window_pos = QPoint(x, y)

        # set the geometry
        self.resize(window_size)
        self.move(window_pos)

        # restore window state
        if settings.has('MainWindow/state'):
            if not self.restoreState(settings.get('MainWindow/state'),
                                     SAVE_STATE_VERSION):
                logger.warning(
                    "The previous layout of workbench is not compatible with this version, reverting to default layout."
                )
        else:
            self.setWindowState(Qt.WindowMaximized)

        # read in settings for children
        AlgorithmInputHistory().readSettings(settings)
        for widget in self.widgets:
            if hasattr(widget, 'readSettingsIfNotDone'):
                widget.readSettingsIfNotDone(settings)

    def writeSettings(self, settings):
        settings.set('MainWindow/size', self.size())  # QSize
        settings.set('MainWindow/position', self.pos())  # QPoint
        settings.set('MainWindow/state',
                     self.saveState(SAVE_STATE_VERSION))  # QByteArray

        # write out settings for children
        AlgorithmInputHistory().writeSettings(settings)
        for widget in self.widgets:
            if hasattr(widget, 'writeSettings'):
                widget.writeSettings(settings)

    def override_python_input(self):
        """Replace python input with a call to a qinputdialog"""
        builtins.input = QAppThreadCall(input_qinputdialog)

    def attempt_to_restore_state(self, state):
        if self.restoreState(state, SAVE_STATE_VERSION):
            return

        # The version number of the supplied state is older than the current version
        reply = QMessageBox.question(
            self, "Layout Restoration",
            "The selected layout is incompatible with this version of Workbench. Workbench will attempt to restore "
            "the layout, but it may appear differently from before.\nDo you wish to continue?",
            QMessageBox.Yes | QMessageBox.No)
        if not reply == QMessageBox.Yes:
            return

        for version in range(0, SAVE_STATE_VERSION):
            if self.restoreState(state, version):
                QMessageBox.information(
                    self, "Success",
                    "The layout was successfully restored.\nTo hide this warning in the future, delete the old "
                    "layout in File > Settings, and save this as a new layout."
                )
                return
        QMessageBox.warning(self, "Failure",
                            "The layout was unable to be restored.",
                            QMessageBox.Ok)
    def setUp(self):
        self.working_directory = tempfile.mkdtemp()

        self.pr = ProjectRecovery(None)
        self.pr_saver = self.pr.saver
class ProjectRecoveryModelTest(unittest.TestCase):
    def setUp(self):
        self.pr = ProjectRecovery(multifileinterpreter=None)

        # Make absolutely sure that the workbench-recovery directory is cleared.
        if os.path.exists(self.pr.recovery_directory):
            shutil.rmtree(self.pr.recovery_directory)

        # Set up some checkpoints
        self.setup_some_checkpoints()

        self.pr._make_process_from_pid = mock.MagicMock()
        self.pr._is_mantid_workbench_process = mock.MagicMock(return_value=True)
        self.prm = ProjectRecoveryModel(self.pr, mock.MagicMock())

    def tearDown(self):
        # Make sure to clear the hostname layer between tests
        ADS.clear()

        if os.path.exists(self.pid):
            shutil.rmtree(self.pid)

    def setup_some_checkpoints(self):
        self.pr._spin_off_another_time_thread = mock.MagicMock()
        directory = self.pr.recovery_directory_hostname

        # Add a numbered folder for the pid
        self.pid = os.path.join(directory, "3000000")
        if not os.path.exists(self.pid):
            os.makedirs(self.pid)
        self.pr._recovery_directory_pid = self.pid

        # Add 5 workspaces
        for ii in range(0, 5):
            CreateSampleWorkspace(OutputWorkspace=str(ii))

        self.pr.saver._spin_off_another_time_thread = mock.MagicMock()
        self.pr.recovery_save()

    def test_find_number_of_workspaces_in_directory(self):
        # Expect 0 as just checkpoints
        self.assertEqual(self.prm.find_number_of_workspaces_in_directory(self.pid), 0)

        self.assertTrue(os.path.exists(self.pid))
        list_dir = os.listdir(self.pid)
        list_dir.sort()
        self.assertEqual(self.prm.find_number_of_workspaces_in_directory(os.path.join(self.pid, list_dir[0])), 5)

    def test_get_row_as_string(self):
        row = self.prm.rows[0]
        self.assertEqual(self.prm.get_row(row[0]), row)

    def test_get_row_as_int(self):
        row = self.prm.rows[0]
        self.assertEqual(self.prm.get_row(0), row)

    def test_get_row_as_string_not_found(self):
        row = ["", "", ""]
        self.assertEqual(self.prm.get_row("asdadasdasd"), row)

    def test_start_mantid_normally(self):
        self.prm.start_mantid_normally()
        self.assertEqual(self.prm.presenter.close_view.call_count, 1)

    def test_recover_selected_checkpoint(self):
        checkpoint = os.listdir(self.pid)[0]
        self.prm._start_recovery_of_checkpoint = mock.MagicMock()
        self.prm.recover_selected_checkpoint(checkpoint)

        self.assertEqual(1, self.prm.presenter.change_start_mantid_to_cancel_label.call_count)
        self.assertEqual(1, self.prm._start_recovery_of_checkpoint.call_count)

    def test_open_selected_in_editor(self):
        checkpoint = os.listdir(self.pid)[0]
        self.prm.project_recovery.open_checkpoint_in_script_editor = mock.MagicMock()
        self.prm.open_selected_in_editor(checkpoint)

        self.assertEqual(1, self.prm.project_recovery.open_checkpoint_in_script_editor.call_count)
        self.assertEqual(self.prm.project_recovery.open_checkpoint_in_script_editor.call_args,
                         mock.call(os.path.join(self.pid, checkpoint)))

    def test_decide_last_checkpoint(self):
        CreateSampleWorkspace(OutputWorkspace="6")
        self.pr.recovery_save()

        checkpoints = os.listdir(self.pid)
        checkpoints.sort()

        last_checkpoint = self.prm.decide_last_checkpoint()
        self.assertEqual(checkpoints[-1], os.path.basename(last_checkpoint))

    def test_fill_rows(self):
        # wait a second so that we can add a second checkpoint with a different name, because the checkpoints differ at
        # most by a second.
        time.sleep(1)

        CreateSampleWorkspace(OutputWorkspace="6")
        self.pr.recovery_save()

        self.prm.fill_rows()

        checkpoints = os.listdir(self.pid)
        checkpoints.sort()

        self.assertEqual(["", "", ""], self.prm.rows[2])
        self.assertEqual([checkpoints[0].replace("T", " "), "5", "No"], self.prm.rows[1])
        self.assertEqual([checkpoints[1].replace("T", " "), "6", "No"], self.prm.rows[0])

    def test_get_number_of_checkpoints(self):
        self.assertEqual(int(ConfigService.getString(NO_OF_CHECKPOINTS_KEY)),
                         self.prm.get_number_of_checkpoints())

    def test_update_checkpoint_tried(self):
        checkpoints = os.listdir(self.pid)

        self.assertEqual(self.prm.rows[0][2], "No")
        self.prm._update_checkpoint_tried(os.path.join(self.pid, checkpoints[0]))
        self.assertEqual(self.prm.rows[0][2], "Yes")