From ce4761a95a3912e6f76d970a52ede8a1bb0364cd Mon Sep 17 00:00:00 2001 From: Louise Fox <208544511+folouiseAWS@users.noreply.github.com> Date: Thu, 11 Dec 2025 10:57:45 -0800 Subject: [PATCH 01/12] feat: Make farm, queue, and storage profile editable in Submit Dialog - Replace read-only DeadlineFarmDisplay and DeadlineQueueDisplay with editable DeadlineFarmListComboBox and DeadlineQueueListComboBox - Add DeadlineStorageProfileNameListComboBox that shows only when queue has storage profiles available - Config updates immediately on selection change - Remove unused display classes and imports - Add unit tests for new combo box functionality Signed-off-by: Louise Fox <208544511+folouiseAWS@users.noreply.github.com> --- .../dialogs/submit_job_to_deadline_dialog.py | 3 + .../ui/widgets/shared_job_settings_tab.py | 324 ++++++++---------- .../widgets/test_shared_job_settings_tab.py | 194 ++++++++++- 3 files changed, 336 insertions(+), 185 deletions(-) diff --git a/src/deadline/client/ui/dialogs/submit_job_to_deadline_dialog.py b/src/deadline/client/ui/dialogs/submit_job_to_deadline_dialog.py index dd566b3d9..939b894d3 100644 --- a/src/deadline/client/ui/dialogs/submit_job_to_deadline_dialog.py +++ b/src/deadline/client/ui/dialogs/submit_job_to_deadline_dialog.py @@ -225,6 +225,9 @@ def _build_ui( self.deadline_authentication_status.api_availability_changed.connect( self.refresh_deadline_settings ) + self.deadline_authentication_status.deadline_config_changed.connect( + self.refresh_deadline_settings + ) # Refresh the submit button enable state once queue parameter status changes self.shared_job_settings.valid_parameters.connect(self._set_submit_button_state) diff --git a/src/deadline/client/ui/widgets/shared_job_settings_tab.py b/src/deadline/client/ui/widgets/shared_job_settings_tab.py index f43365823..84652dc50 100644 --- a/src/deadline/client/ui/widgets/shared_job_settings_tab.py +++ b/src/deadline/client/ui/widgets/shared_job_settings_tab.py @@ -6,7 +6,6 @@ from __future__ import annotations -import sys import threading from typing import Any, Dict, Optional @@ -15,7 +14,6 @@ QComboBox, QFormLayout, QGroupBox, - QHBoxLayout, QLabel, QLineEdit, QRadioButton, @@ -24,8 +22,7 @@ QWidget, ) -from ... import api -from ...config import get_setting +from ...config import get_setting, set_setting, config_file from .._utils import CancelationFlag, tr from .openjd_parameters_widget import OpenJDParametersWidget from ...api import get_queue_parameter_definitions @@ -493,208 +490,167 @@ def _build_ui(self): """ Build the UI for the Deadline settings """ + # Import combo box classes from deadline_config_dialog + from ..dialogs.deadline_config_dialog import ( + DeadlineFarmListComboBox, + DeadlineQueueListComboBox, + DeadlineStorageProfileNameListComboBox, + ) + self.farm_box_label = QLabel(tr("Farm")) - self.farm_box = DeadlineFarmDisplay() + self.farm_box = DeadlineFarmListComboBox(parent=self) self.layout.addRow(self.farm_box_label, self.farm_box) self.queue_box_label = QLabel(tr("Queue")) - self.queue_box = DeadlineQueueDisplay() + self.queue_box = DeadlineQueueListComboBox(parent=self) self.layout.addRow(self.queue_box_label, self.queue_box) - def refresh_setting_controls(self, deadline_authorized): - """ - Refreshes the controls for UI items that depend on the AWS Deadline Cloud API - for their values. - - Args: - deadline_authorized (bool): Should be the result of a call to - api.check_deadline_available, for example from - an AWS Deadline Cloud Status Widget. - """ - self.farm_box.refresh(deadline_authorized) - self.queue_box.refresh(deadline_authorized) - - -class _DeadlineNamedResourceDisplay(QWidget): - """ - A Label for displaying an AWS Deadline Cloud resource, that starts displaying - it as the Id, but does an async call to AWS Deadline Cloud to convert it - to the name. - - Args: - resource_name (str): The resource name for the list, like "Farm", - "Queue", "Fleet". - setting_name (str): The setting name for the item. - """ + self.storage_profile_box_label = QLabel(tr("Default storage profile")) + self.storage_profile_box = DeadlineStorageProfileNameListComboBox(parent=self) + self.layout.addRow(self.storage_profile_box_label, self.storage_profile_box) - # Emitted when the background refresh thread catches an exception, - # provides (operation_name, BaseException) - background_exception = Signal(str, BaseException) + # Hide storage profile by default - only show when queue has storage profiles + self._set_storage_profile_visible(False) - # Emitted when an async refresh_item thread completes, - # provides (refresh_id, id, name, description) - _item_update = Signal(int, str, str, str) + # Connect signals for farm, queue, and storage profile changes + self.farm_box.box.currentIndexChanged.connect(self._on_farm_changed) + self.queue_box.box.currentIndexChanged.connect(self._on_queue_changed) + self.storage_profile_box.box.currentIndexChanged.connect(self._on_storage_profile_changed) - def __init__(self, *, resource_name, setting_name, parent: Optional[QWidget] = None): - super().__init__(parent=parent) - - self.__refresh_thread = None - self.__refresh_id = 0 - self.canceled = CancelationFlag() - self.destroyed.connect(self.canceled.set_canceled) - - self.resource_name = resource_name - self.setting_name = setting_name - self.item_id = get_setting(self.setting_name) - self.item_name = "" - self.item_description = "" - - self._build_ui() - - self.label.setText(self.item_display_name()) - - def _build_ui(self): - self.label = QLabel(parent=self) - layout = QHBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(self.label) - self._item_update.connect(self.handle_item_update) - self.background_exception.connect(self.handle_background_exception) - - def handle_background_exception(self, e): - self.label.setText(self.item_id) - self.label.setToolTip("") - - def item_display_name(self): - """Returns the text to display the item name as""" - return self.item_name or self.item_id or "" - - def refresh(self, deadline_authorized): - """ - Starts a background thread to refresh the item name. - - Args: - deadline_authorized (bool): Should be the result of a call to - api.check_deadline_available, for example from - an AWS Deadline Cloud Status Widget. - """ - resource_id = get_setting(self.setting_name) - if resource_id != self.item_id or not self.item_name: - self.item_id = resource_id - self.item_name = "" - self.item_description = "" - display_name = self.item_display_name() - # Only call the AWS Deadline Cloud API if we've confirmed access - if deadline_authorized: - display_name = " - " + display_name - - self.__refresh_id += 1 - self.__refresh_thread = threading.Thread( - target=self._refresh_thread_function, - name=f"AWS Deadline Cloud refresh {self.resource_name} item thread", - args=(self.__refresh_id,), - ) - self.__refresh_thread.start() - - self.label.setText(display_name) - self.label.setToolTip(self.item_description) - else: - self.label.setText(self.item_display_name()) + # Connect to storage profile combo box model to detect when list changes + self.storage_profile_box.box.model().rowsInserted.connect( + self._update_storage_profile_visibility + ) + self.storage_profile_box.box.model().rowsRemoved.connect( + self._update_storage_profile_visibility + ) + self.storage_profile_box.box.model().modelReset.connect( + self._update_storage_profile_visibility + ) - def handle_item_update(self, refresh_id, id, name, description): - # Apply the refresh if it's still for the latest call - if refresh_id == self.__refresh_id: - self.item_id = id - self.item_name = name - self.item_description = description - self.label.setText(self.item_display_name()) - self.label.setToolTip(self.item_description) - - def _refresh_thread_function(self, refresh_id: int): - """ - This function gets started in a background thread to refresh the list. - """ - try: - item = self.get_item() - if not self.canceled: - self._item_update.emit(refresh_id, *item) - except BaseException as e: - if not self.canceled: - self.background_exception.emit(f"Refresh {self.resource_name} item", e) + # Initialize with current config + config = config_file.read_config() + self.farm_box.set_config(config) + self.queue_box.set_config(config) + self.storage_profile_box.set_config(config) + + def _set_storage_profile_visible(self, visible: bool): + """Show or hide the storage profile selector""" + self.storage_profile_box_label.setVisible(visible) + self.storage_profile_box.setVisible(visible) + + def _update_storage_profile_visibility(self): + """Update storage profile visibility based on available profiles""" + # Check if there are actual storage profiles (not just placeholder items) + count = self.storage_profile_box.box.count() + # Hide if empty or only has "" or "" placeholder + has_real_profiles = count > 0 and self.storage_profile_box.box.itemData(0) not in ( + None, + "", + ) + # Also check if it's just a refreshing placeholder + if count == 1 and self.storage_profile_box.box.itemText(0) in ( + "", + "", + ): + has_real_profiles = False + self._set_storage_profile_visible(has_real_profiles) + def _on_farm_changed(self, index: int): + """Handle farm selection change in Submit Dialog""" + if index < 0: + return -class DeadlineFarmDisplay(_DeadlineNamedResourceDisplay): - def __init__(self, *, parent: Optional[QWidget] = None): - super().__init__(resource_name="Farm", setting_name="defaults.farm_id", parent=parent) - - def get_item(self): - farm_id = get_setting(self.setting_name) - if farm_id: - deadline = api.get_boto3_client("deadline") - response = deadline.get_farm(farmId=farm_id) - return (response["farmId"], response["displayName"], response["description"]) - else: - return ("", "", "") + # Get the selected farm ID from the combo box + farm_id = self.farm_box.box.itemData(index) + if farm_id is None: + return + # Update config immediately (unlike Settings Dialog which defers to apply()) + set_setting("defaults.farm_id", farm_id) -class DeadlineQueueDisplay(_DeadlineNamedResourceDisplay): - def __init__(self, *, parent: Optional[QWidget] = None): - super().__init__(resource_name="Queue", setting_name="defaults.queue_id", parent=parent) + # Refresh queue list for the new farm (same as Settings Dialog) + self.queue_box.refresh_list() - def get_item(self): - farm_id = get_setting("defaults.farm_id") - queue_id = get_setting(self.setting_name) - if farm_id and queue_id: - deadline = api.get_boto3_client("deadline") - response = deadline.get_queue(farmId=farm_id, queueId=queue_id) - return (response["queueId"], response["displayName"], response["description"]) - else: - return ("", "", "") + # Notify parent to refresh (triggers submit button state update and queue parameters) + self._notify_parent_refresh() + def _on_queue_changed(self, index: int): + """Handle queue selection change in Submit Dialog""" + if index < 0: + return -class DeadlineStorageProfileNameDisplay(_DeadlineNamedResourceDisplay): - WINDOWS_OS = "Windows" - MAC_OS = "Macos" - LINUX_OS = "Linux" + # Get the selected queue ID from the combo box + queue_id = self.queue_box.box.itemData(index) + if queue_id is None: + return - def __init__(self, *, parent: Optional[QWidget] = None): - super().__init__( - resource_name="Storage profile name", - setting_name="settings.storage_profile_id", - parent=parent, - ) + # Update config immediately (unlike Settings Dialog which defers to apply()) + set_setting("defaults.queue_id", queue_id) - def get_item(self): - farm_id = get_setting("defaults.farm_id") - queue_id = get_setting("defaults.queue_id") - storage_profile_id = get_setting(self.setting_name) + # Refresh storage profile list for the new queue + self.storage_profile_box.refresh_list() - if farm_id and queue_id and storage_profile_id: - deadline = api.get_boto3_client("deadline") - response = deadline.list_storage_profiles_for_queue(farmId=farm_id, queueId=queue_id) - farm_storage_profiles = response.get("storageProfiles", {}) + # Notify parent to refresh (triggers submit button state update and queue parameters) + self._notify_parent_refresh() - if farm_storage_profiles: - storage_profile = [ - (item["storageProfileId"], item["displayName"], item["osFamily"]) - for item in farm_storage_profiles - if storage_profile_id == item["storageProfileId"] - ] - return storage_profile[0] + def _on_storage_profile_changed(self, index: int): + """Handle storage profile selection change in Submit Dialog""" + if index < 0: + return - return ("", "", "") + # Get the selected storage profile ID from the combo box + storage_profile_id = self.storage_profile_box.box.itemData(index) + + # Update config immediately + set_setting("settings.storage_profile_id", storage_profile_id if storage_profile_id else "") + + def _notify_parent_refresh(self): + """Helper to notify parent widgets to refresh after config changes""" + # Find SharedJobSettingsWidget parent to refresh queue parameters + parent_widget = self.parent() + while parent_widget is not None: + if hasattr(parent_widget, "refresh_queue_parameters"): + parent_widget.refresh_queue_parameters() + if hasattr(parent_widget, "parent") and callable(parent_widget.parent): + parent_widget = parent_widget.parent() + else: + break + + # Find SubmitJobToDeadlineDialog to refresh submit button state + parent_widget = self.parent() + while parent_widget is not None: + if hasattr(parent_widget, "refresh_deadline_settings"): + parent_widget.refresh_deadline_settings() + break + if hasattr(parent_widget, "parent") and callable(parent_widget.parent): + parent_widget = parent_widget.parent() + else: + break - def _get_default_storage_profile_name(self) -> str: - """ - Get a string specifying what the OS is, following the format the Deadline storage profile API expects. + def refresh_setting_controls(self, deadline_authorized): """ - if sys.platform.startswith("linux"): - return self.LINUX_OS - - if sys.platform.startswith("darwin"): - return self.MAC_OS - - if sys.platform.startswith("win"): - return self.WINDOWS_OS + Refreshes the controls for UI items that depend on the AWS Deadline Cloud API + for their values. - return "" + Args: + deadline_authorized (bool): Should be the result of a call to + api.check_deadline_available, for example from + an AWS Deadline Cloud Status Widget. + """ + # Update config for combo boxes + config = config_file.read_config() + self.farm_box.set_config(config) + self.queue_box.set_config(config) + self.storage_profile_box.set_config(config) + + # Refresh selected items to reflect current config + self.farm_box.refresh_selected_id() + self.queue_box.refresh_selected_id() + self.storage_profile_box.refresh_selected_id() + + # Refresh lists if authorized + if deadline_authorized: + self.farm_box.refresh_list() + self.queue_box.refresh_list() + self.storage_profile_box.refresh_list() diff --git a/test/unit/deadline_client/ui/widgets/test_shared_job_settings_tab.py b/test/unit/deadline_client/ui/widgets/test_shared_job_settings_tab.py index 477503dc9..81ead0e5b 100644 --- a/test/unit/deadline_client/ui/widgets/test_shared_job_settings_tab.py +++ b/test/unit/deadline_client/ui/widgets/test_shared_job_settings_tab.py @@ -1,9 +1,13 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. import pytest +from unittest.mock import patch, MagicMock try: - from deadline.client.ui.widgets.shared_job_settings_tab import SharedJobSettingsWidget + from deadline.client.ui.widgets.shared_job_settings_tab import ( + SharedJobSettingsWidget, + DeadlineCloudSettingsWidget, + ) from deadline.client.ui.dataclasses import JobBundleSettings except ImportError: # The tests in this file should be skipped if Qt UI related modules cannot be loaded @@ -80,3 +84,191 @@ def test_max_worker_count_should_be_integer_within_range( ): shared_job_settings_tab.shared_job_properties_box.max_worker_count_box.setValue(-1) assert shared_job_settings_tab.shared_job_properties_box.max_worker_count_box.value() == 1 + + +# Tests for DeadlineCloudSettingsWidget farm/queue combo boxes + + +@pytest.fixture(scope="function") +def deadline_cloud_settings_widget(qtbot) -> DeadlineCloudSettingsWidget: + """Fixture for DeadlineCloudSettingsWidget with mocked combo boxes.""" + with patch( + "deadline.client.ui.widgets.shared_job_settings_tab.config_file.read_config" + ) as mock_config: + mock_config.return_value = MagicMock() + widget = DeadlineCloudSettingsWidget() + qtbot.addWidget(widget) + return widget + + +def test_farm_selection_updates_config(deadline_cloud_settings_widget: DeadlineCloudSettingsWidget): + """Test that selecting a farm updates the config setting.""" + widget = deadline_cloud_settings_widget + test_farm_id = "farm-test123" + + # Add a test item to the farm combo box + widget.farm_box.box.addItem("Test Farm", test_farm_id) + + with patch( + "deadline.client.ui.widgets.shared_job_settings_tab.set_setting" + ) as mock_set_setting: + # Select the farm (triggers _on_farm_changed) + widget.farm_box.box.setCurrentIndex(widget.farm_box.box.count() - 1) + + # Verify config was updated with the farm ID + mock_set_setting.assert_called_with("defaults.farm_id", test_farm_id) + + +def test_queue_selection_updates_config( + deadline_cloud_settings_widget: DeadlineCloudSettingsWidget, +): + """Test that selecting a queue updates the config setting.""" + widget = deadline_cloud_settings_widget + test_queue_id = "queue-test456" + + # Add a test item to the queue combo box + widget.queue_box.box.addItem("Test Queue", test_queue_id) + + with patch( + "deadline.client.ui.widgets.shared_job_settings_tab.set_setting" + ) as mock_set_setting: + # Select the queue (triggers _on_queue_changed) + widget.queue_box.box.setCurrentIndex(widget.queue_box.box.count() - 1) + + # Verify config was updated with the queue ID + mock_set_setting.assert_called_with("defaults.queue_id", test_queue_id) + + +def test_farm_change_refreshes_queue_list( + deadline_cloud_settings_widget: DeadlineCloudSettingsWidget, +): + """Test that changing farm triggers queue list refresh.""" + widget = deadline_cloud_settings_widget + test_farm_id = "farm-test789" + + # Add a test item to the farm combo box + widget.farm_box.box.addItem("Test Farm", test_farm_id) + + with patch.object(widget.queue_box, "refresh_list") as mock_refresh: + with patch("deadline.client.ui.widgets.shared_job_settings_tab.set_setting"): + # Select the farm + widget.farm_box.box.setCurrentIndex(widget.farm_box.box.count() - 1) + + # Verify queue list was refreshed + mock_refresh.assert_called_once() + + +def test_refresh_setting_controls_updates_combo_boxes( + deadline_cloud_settings_widget: DeadlineCloudSettingsWidget, +): + """Test that refresh_setting_controls updates both combo boxes.""" + widget = deadline_cloud_settings_widget + + with patch.object(widget.farm_box, "set_config") as mock_farm_config, patch.object( + widget.queue_box, "set_config" + ) as mock_queue_config, patch.object( + widget.farm_box, "refresh_selected_id" + ) as mock_farm_refresh, patch.object( + widget.queue_box, "refresh_selected_id" + ) as mock_queue_refresh, patch.object( + widget.farm_box, "refresh_list" + ) as mock_farm_list, patch.object(widget.queue_box, "refresh_list") as mock_queue_list, patch( + "deadline.client.ui.widgets.shared_job_settings_tab.config_file.read_config" + ): + # Call refresh with authorized=True + widget.refresh_setting_controls(deadline_authorized=True) + + # Verify all methods were called + mock_farm_config.assert_called_once() + mock_queue_config.assert_called_once() + mock_farm_refresh.assert_called_once() + mock_queue_refresh.assert_called_once() + mock_farm_list.assert_called_once() + mock_queue_list.assert_called_once() + + +def test_refresh_setting_controls_skips_list_refresh_when_unauthorized( + deadline_cloud_settings_widget: DeadlineCloudSettingsWidget, +): + """Test that refresh_setting_controls skips list refresh when not authorized.""" + widget = deadline_cloud_settings_widget + + with patch.object(widget.farm_box, "set_config"), patch.object( + widget.queue_box, "set_config" + ), patch.object(widget.farm_box, "refresh_selected_id"), patch.object( + widget.queue_box, "refresh_selected_id" + ), patch.object(widget.farm_box, "refresh_list") as mock_farm_list, patch.object( + widget.queue_box, "refresh_list" + ) as mock_queue_list, patch( + "deadline.client.ui.widgets.shared_job_settings_tab.config_file.read_config" + ): + # Call refresh with authorized=False + widget.refresh_setting_controls(deadline_authorized=False) + + # Verify list refresh was NOT called + mock_farm_list.assert_not_called() + mock_queue_list.assert_not_called() + + +def test_storage_profile_hidden_by_default( + deadline_cloud_settings_widget: DeadlineCloudSettingsWidget, +): + """Test that storage profile selector is hidden by default.""" + widget = deadline_cloud_settings_widget + + # Storage profile should be hidden initially + assert not widget.storage_profile_box.isVisible() + assert not widget.storage_profile_box_label.isVisible() + + +def test_storage_profile_shown_when_profiles_available( + deadline_cloud_settings_widget: DeadlineCloudSettingsWidget, +): + """Test that storage profile selector is shown when profiles are available.""" + widget = deadline_cloud_settings_widget + + # Add a real storage profile item + widget.storage_profile_box.box.addItem("Test Profile", "sp-test123") + + # Storage profile should now be visible + assert widget.storage_profile_box.isVisible() + assert widget.storage_profile_box_label.isVisible() + + +def test_storage_profile_selection_updates_config( + deadline_cloud_settings_widget: DeadlineCloudSettingsWidget, +): + """Test that selecting a storage profile updates the config setting.""" + widget = deadline_cloud_settings_widget + test_profile_id = "sp-test123" + + # Add a test item to the storage profile combo box + widget.storage_profile_box.box.addItem("Test Profile", test_profile_id) + + with patch( + "deadline.client.ui.widgets.shared_job_settings_tab.set_setting" + ) as mock_set_setting: + # Select the storage profile (triggers _on_storage_profile_changed) + widget.storage_profile_box.box.setCurrentIndex(widget.storage_profile_box.box.count() - 1) + + # Verify config was updated with the storage profile ID + mock_set_setting.assert_called_with("settings.storage_profile_id", test_profile_id) + + +def test_queue_change_refreshes_storage_profile_list( + deadline_cloud_settings_widget: DeadlineCloudSettingsWidget, +): + """Test that changing queue triggers storage profile list refresh.""" + widget = deadline_cloud_settings_widget + test_queue_id = "queue-test789" + + # Add a test item to the queue combo box + widget.queue_box.box.addItem("Test Queue", test_queue_id) + + with patch.object(widget.storage_profile_box, "refresh_list") as mock_refresh: + with patch("deadline.client.ui.widgets.shared_job_settings_tab.set_setting"): + # Select the queue + widget.queue_box.box.setCurrentIndex(widget.queue_box.box.count() - 1) + + # Verify storage profile list was refreshed + mock_refresh.assert_called_once() From de563972ae6bfcb25a0b92ceb84f51ce8350daee Mon Sep 17 00:00:00 2001 From: Louise Fox <208544511+folouiseAWS@users.noreply.github.com> Date: Thu, 11 Dec 2025 14:46:35 -0800 Subject: [PATCH 02/12] fix: refresh queue parameters when farm changes Previously, queue parameters would only refresh when the queue ID changed. This fix ensures queue environments are also refreshed when the farm ID changes, since queue environments are specific to a farm+queue combination. Signed-off-by: Louise Fox <208544511+folouiseAWS@users.noreply.github.com> --- .../ui/widgets/shared_job_settings_tab.py | 11 ++- .../widgets/test_shared_job_settings_tab.py | 91 +++++++++++++++++++ 2 files changed, 97 insertions(+), 5 deletions(-) diff --git a/src/deadline/client/ui/widgets/shared_job_settings_tab.py b/src/deadline/client/ui/widgets/shared_job_settings_tab.py index 84652dc50..b4a9a5e41 100644 --- a/src/deadline/client/ui/widgets/shared_job_settings_tab.py +++ b/src/deadline/client/ui/widgets/shared_job_settings_tab.py @@ -128,24 +128,25 @@ def refresh_ui(self, job_settings: Any, load_new_bundle: bool = False): def refresh_queue_parameters(self, load_new_bundle: bool = False): """ - If the default queue id or job bundle has changed, refresh the queue parameters. + If the default farm id, queue id, or job bundle has changed, refresh the queue parameters. """ farm_id = get_setting("defaults.farm_id") queue_id = get_setting("defaults.queue_id") if not farm_id or not queue_id: self.queue_parameters_box.rebuild_ui(async_loading_state="") return # If the user has not selected a farm or queue ID, don't try to load + farm_or_queue_changed = farm_id != self.farm_id or queue_id != self.queue_id if ( self.queue_parameters_box.async_loading_state - or queue_id != self.queue_id + or farm_or_queue_changed or load_new_bundle ): self.queue_parameters_box.rebuild_ui( async_loading_state="Reloading Queue Environments..." ) - # Join the thread if the queue id or job bundle has changed and the thread is running + # Join the thread if the farm, queue id, or job bundle has changed and the thread is running if ( - (queue_id != self.queue_id or load_new_bundle) + (farm_or_queue_changed or load_new_bundle) and self.__refresh_queue_parameters_thread and self.__refresh_queue_parameters_thread.is_alive() ): @@ -490,7 +491,7 @@ def _build_ui(self): """ Build the UI for the Deadline settings """ - # Import combo box classes from deadline_config_dialog + # Import here to avoid circular import from ..dialogs.deadline_config_dialog import ( DeadlineFarmListComboBox, DeadlineQueueListComboBox, diff --git a/test/unit/deadline_client/ui/widgets/test_shared_job_settings_tab.py b/test/unit/deadline_client/ui/widgets/test_shared_job_settings_tab.py index 81ead0e5b..911f31173 100644 --- a/test/unit/deadline_client/ui/widgets/test_shared_job_settings_tab.py +++ b/test/unit/deadline_client/ui/widgets/test_shared_job_settings_tab.py @@ -272,3 +272,94 @@ def test_queue_change_refreshes_storage_profile_list( # Verify storage profile list was refreshed mock_refresh.assert_called_once() + + +# Tests for SharedJobSettingsWidget.refresh_queue_parameters + + +def test_refresh_queue_parameters_triggers_on_farm_change( + shared_job_settings_tab: SharedJobSettingsWidget, +): + widget = shared_job_settings_tab + + # Set initial farm and queue IDs + widget.farm_id = "farm-initial" + widget.queue_id = "queue-initial" + + with patch( + "deadline.client.ui.widgets.shared_job_settings_tab.get_setting" + ) as mock_get_setting: + # Simulate farm change (different farm_id, same queue_id) + mock_get_setting.side_effect = lambda key: { + "defaults.farm_id": "farm-new", + "defaults.queue_id": "queue-initial", + }.get(key) + + with patch.object(widget.queue_parameters_box, "rebuild_ui") as mock_rebuild, patch.object( + widget, "_start_load_queue_parameters_thread" + ) as mock_start_thread: + widget.refresh_queue_parameters() + + mock_rebuild.assert_called_once_with( + async_loading_state="Reloading Queue Environments..." + ) + mock_start_thread.assert_called_once() + + +def test_refresh_queue_parameters_triggers_on_queue_change( + shared_job_settings_tab: SharedJobSettingsWidget, +): + widget = shared_job_settings_tab + + # Set initial farm and queue IDs + widget.farm_id = "farm-initial" + widget.queue_id = "queue-initial" + + with patch( + "deadline.client.ui.widgets.shared_job_settings_tab.get_setting" + ) as mock_get_setting: + # Simulate queue change (same farm_id, different queue_id) + mock_get_setting.side_effect = lambda key: { + "defaults.farm_id": "farm-initial", + "defaults.queue_id": "queue-new", + }.get(key) + + with patch.object(widget.queue_parameters_box, "rebuild_ui") as mock_rebuild, patch.object( + widget, "_start_load_queue_parameters_thread" + ) as mock_start_thread: + widget.refresh_queue_parameters() + + mock_rebuild.assert_called_once_with( + async_loading_state="Reloading Queue Environments..." + ) + mock_start_thread.assert_called_once() + + +def test_refresh_queue_parameters_no_refresh_when_unchanged( + shared_job_settings_tab: SharedJobSettingsWidget, +): + widget = shared_job_settings_tab + + # Set initial farm and queue IDs + widget.farm_id = "farm-same" + widget.queue_id = "queue-same" + + # Clear the async loading state so it doesn't trigger refresh + widget.queue_parameters_box.async_loading_state = "" + + with patch( + "deadline.client.ui.widgets.shared_job_settings_tab.get_setting" + ) as mock_get_setting: + # Same farm and queue IDs + mock_get_setting.side_effect = lambda key: { + "defaults.farm_id": "farm-same", + "defaults.queue_id": "queue-same", + }.get(key) + + with patch.object(widget.queue_parameters_box, "rebuild_ui") as mock_rebuild, patch.object( + widget, "_start_load_queue_parameters_thread" + ) as mock_start_thread: + widget.refresh_queue_parameters() + + mock_rebuild.assert_not_called() + mock_start_thread.assert_not_called() From 99387ba2212ac01b2a2585f5eb0dcc726a778c5e Mon Sep 17 00:00:00 2001 From: Louise Fox <208544511+folouiseAWS@users.noreply.github.com> Date: Tue, 16 Dec 2025 16:28:58 -0800 Subject: [PATCH 03/12] fix: storage profile visibility in Submit Dialog The storage profile selector was not showing in the Submit Dialog even when profiles were available. Two issues were fixed: 1. _update_storage_profile_visibility() only checked itemData(0), but sorts alphabetically first and has empty string data, so it always returned False. Now iterates through all items to find any with real profile data. 2. Model signals (rowsInserted, etc.) weren't firing because the base class uses block_signals when populating the combo box. Added a connection to the _list_update signal which fires after async refresh. Signed-off-by: Louise Fox <208544511+folouiseAWS@users.noreply.github.com> --- .../ui/widgets/shared_job_settings_tab.py | 26 ++++++++++------- .../widgets/test_shared_job_settings_tab.py | 29 +++++++++++++++++++ 2 files changed, 44 insertions(+), 11 deletions(-) diff --git a/src/deadline/client/ui/widgets/shared_job_settings_tab.py b/src/deadline/client/ui/widgets/shared_job_settings_tab.py index b4a9a5e41..a77824de3 100644 --- a/src/deadline/client/ui/widgets/shared_job_settings_tab.py +++ b/src/deadline/client/ui/widgets/shared_job_settings_tab.py @@ -528,6 +528,10 @@ def _build_ui(self): self.storage_profile_box.box.model().modelReset.connect( self._update_storage_profile_visibility ) + # Also connect to the _list_update signal to handle updates after async refresh + self.storage_profile_box._list_update.connect( + lambda *args: self._update_storage_profile_visibility() + ) # Initialize with current config config = config_file.read_config() @@ -543,18 +547,18 @@ def _set_storage_profile_visible(self, visible: bool): def _update_storage_profile_visibility(self): """Update storage profile visibility based on available profiles""" # Check if there are actual storage profiles (not just placeholder items) + # Note: "" sorts first alphabetically and has empty string as data, + # so we need to check if there are items with non-empty data count = self.storage_profile_box.box.count() - # Hide if empty or only has "" or "" placeholder - has_real_profiles = count > 0 and self.storage_profile_box.box.itemData(0) not in ( - None, - "", - ) - # Also check if it's just a refreshing placeholder - if count == 1 and self.storage_profile_box.box.itemText(0) in ( - "", - "", - ): - has_real_profiles = False + has_real_profiles = False + for i in range(count): + item_data = self.storage_profile_box.box.itemData(i) + item_text = self.storage_profile_box.box.itemText(i) + # Skip placeholder items + if item_text in ("", "") or item_data in (None, ""): + continue + has_real_profiles = True + break self._set_storage_profile_visible(has_real_profiles) def _on_farm_changed(self, index: int): diff --git a/test/unit/deadline_client/ui/widgets/test_shared_job_settings_tab.py b/test/unit/deadline_client/ui/widgets/test_shared_job_settings_tab.py index 911f31173..e9ad7f75b 100644 --- a/test/unit/deadline_client/ui/widgets/test_shared_job_settings_tab.py +++ b/test/unit/deadline_client/ui/widgets/test_shared_job_settings_tab.py @@ -235,6 +235,35 @@ def test_storage_profile_shown_when_profiles_available( assert widget.storage_profile_box_label.isVisible() +def test_storage_profile_shown_when_none_selected_sorts_first( + deadline_cloud_settings_widget: DeadlineCloudSettingsWidget, +): + """Test that storage profile is visible even when sorts to first position. + + This tests the _list_update signal path which is used during async refresh. + The _list_update signal handler populates the combo box with block_signals, + so model signals don't fire - we need to explicitly trigger visibility update. + """ + widget = deadline_cloud_settings_widget + + # Simulate the async refresh completing with sorted profiles where + # "" sorts first because '<' comes before letters alphabetically + items_list = [ + ("", ""), + ("Profile A", "sp-profile-a"), + ("Profile B", "sp-profile-b"), + ] + + # Emit the _list_update signal to simulate async refresh completing + # This is the code path that was broken - the combo box is populated + # inside block_signals so model signals don't fire + widget.storage_profile_box._list_update.emit(1, items_list) + + # Storage profile should be visible because there are real profiles + assert widget.storage_profile_box.isVisible() + assert widget.storage_profile_box_label.isVisible() + + def test_storage_profile_selection_updates_config( deadline_cloud_settings_widget: DeadlineCloudSettingsWidget, ): From 749b8427c34f4bfd4af6c6f9c36b82b124a49731 Mon Sep 17 00:00:00 2001 From: Louise Fox <208544511+folouiseAWS@users.noreply.github.com> Date: Tue, 20 Jan 2026 12:59:01 -0800 Subject: [PATCH 04/12] fix: rename storage profile label in Submit Dialog Changed 'Default storage profile' to 'Storage profile' in the shared job settings tab and added translations for all supported locales. Signed-off-by: Louise Fox <208544511+folouiseAWS@users.noreply.github.com> --- src/deadline/client/ui/translations/locales/de_DE.json | 1 + src/deadline/client/ui/translations/locales/en_US.json | 1 + src/deadline/client/ui/translations/locales/es_ES.json | 1 + src/deadline/client/ui/translations/locales/fr_FR.json | 1 + src/deadline/client/ui/translations/locales/id_ID.json | 1 + src/deadline/client/ui/translations/locales/it_IT.json | 1 + src/deadline/client/ui/translations/locales/ja_JP.json | 1 + src/deadline/client/ui/translations/locales/ko_KR.json | 1 + src/deadline/client/ui/translations/locales/pt_BR.json | 1 + src/deadline/client/ui/translations/locales/tr_TR.json | 1 + src/deadline/client/ui/translations/locales/zh_CN.json | 1 + src/deadline/client/ui/translations/locales/zh_TW.json | 1 + src/deadline/client/ui/widgets/shared_job_settings_tab.py | 2 +- 13 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/deadline/client/ui/translations/locales/de_DE.json b/src/deadline/client/ui/translations/locales/de_DE.json index 3acc0c960..70bc9ce67 100644 --- a/src/deadline/client/ui/translations/locales/de_DE.json +++ b/src/deadline/client/ui/translations/locales/de_DE.json @@ -36,6 +36,7 @@ "Default farm": "Standard-Farm", "Default queue": "Standard-Warteschlange", "Default storage profile": "Standard-Speicherprofil", + "Storage profile": "Speicherprofil", "Delete": "Löschen", "Description": "Beschreibung", "Do not ask again": "Nicht erneut fragen", diff --git a/src/deadline/client/ui/translations/locales/en_US.json b/src/deadline/client/ui/translations/locales/en_US.json index 97bed802e..f000e31b3 100644 --- a/src/deadline/client/ui/translations/locales/en_US.json +++ b/src/deadline/client/ui/translations/locales/en_US.json @@ -36,6 +36,7 @@ "Default farm": "Default farm", "Default queue": "Default queue", "Default storage profile": "Default storage profile", + "Storage profile": "Storage profile", "Delete": "Delete", "Description": "Description", "Do not ask again": "Do not ask again", diff --git a/src/deadline/client/ui/translations/locales/es_ES.json b/src/deadline/client/ui/translations/locales/es_ES.json index ec7aefb04..c5e05a665 100644 --- a/src/deadline/client/ui/translations/locales/es_ES.json +++ b/src/deadline/client/ui/translations/locales/es_ES.json @@ -36,6 +36,7 @@ "Default farm": "Granja predeterminada", "Default queue": "Cola predeterminada", "Default storage profile": "Perfil de almacenamiento predeterminado", + "Storage profile": "Perfil de almacenamiento", "Delete": "Eliminar", "Description": "Descripción", "Do not ask again": "No volver a preguntar", diff --git a/src/deadline/client/ui/translations/locales/fr_FR.json b/src/deadline/client/ui/translations/locales/fr_FR.json index e8edb1cfb..01931f97a 100644 --- a/src/deadline/client/ui/translations/locales/fr_FR.json +++ b/src/deadline/client/ui/translations/locales/fr_FR.json @@ -36,6 +36,7 @@ "Default farm": "Ferme par défaut", "Default queue": "File d'attente par défaut", "Default storage profile": "Profil de stockage par défaut", + "Storage profile": "Profil de stockage", "Delete": "Supprimer", "Description": "Description", "Do not ask again": "Ne plus demander", diff --git a/src/deadline/client/ui/translations/locales/id_ID.json b/src/deadline/client/ui/translations/locales/id_ID.json index 4b3c1e41a..1407338d5 100644 --- a/src/deadline/client/ui/translations/locales/id_ID.json +++ b/src/deadline/client/ui/translations/locales/id_ID.json @@ -36,6 +36,7 @@ "Default farm": "Peternakan default", "Default queue": "Antrian default", "Default storage profile": "Profil penyimpanan default", + "Storage profile": "Profil penyimpanan", "Delete": "Hapus", "Description": "Deskripsi", "Do not ask again": "Jangan tanya lagi", diff --git a/src/deadline/client/ui/translations/locales/it_IT.json b/src/deadline/client/ui/translations/locales/it_IT.json index efed438f1..8178f87bb 100644 --- a/src/deadline/client/ui/translations/locales/it_IT.json +++ b/src/deadline/client/ui/translations/locales/it_IT.json @@ -36,6 +36,7 @@ "Default farm": "Farm predefinita", "Default queue": "Coda predefinita", "Default storage profile": "Profilo di archiviazione predefinito", + "Storage profile": "Profilo di archiviazione", "Delete": "Elimina", "Description": "Descrizione", "Do not ask again": "Non chiedere più", diff --git a/src/deadline/client/ui/translations/locales/ja_JP.json b/src/deadline/client/ui/translations/locales/ja_JP.json index b018acf58..27e66a9c1 100644 --- a/src/deadline/client/ui/translations/locales/ja_JP.json +++ b/src/deadline/client/ui/translations/locales/ja_JP.json @@ -36,6 +36,7 @@ "Default farm": "デフォルトファーム", "Default queue": "デフォルトキュー", "Default storage profile": "デフォルトストレージプロファイル", + "Storage profile": "ストレージプロファイル", "Delete": "削除", "Description": "説明", "Do not ask again": "今後表示しない", diff --git a/src/deadline/client/ui/translations/locales/ko_KR.json b/src/deadline/client/ui/translations/locales/ko_KR.json index 95844d2d6..cfaca2b44 100644 --- a/src/deadline/client/ui/translations/locales/ko_KR.json +++ b/src/deadline/client/ui/translations/locales/ko_KR.json @@ -36,6 +36,7 @@ "Default farm": "기본 팜", "Default queue": "기본 대기열", "Default storage profile": "기본 스토리지 프로필", + "Storage profile": "스토리지 프로필", "Delete": "삭제", "Description": "설명", "Do not ask again": "다시 묻지 않음", diff --git a/src/deadline/client/ui/translations/locales/pt_BR.json b/src/deadline/client/ui/translations/locales/pt_BR.json index 5d8637de4..c6066880a 100644 --- a/src/deadline/client/ui/translations/locales/pt_BR.json +++ b/src/deadline/client/ui/translations/locales/pt_BR.json @@ -36,6 +36,7 @@ "Default farm": "Fazenda padrão", "Default queue": "Fila padrão", "Default storage profile": "Perfil de armazenamento padrão", + "Storage profile": "Perfil de armazenamento", "Delete": "Excluir", "Description": "Descrição", "Do not ask again": "Não perguntar novamente", diff --git a/src/deadline/client/ui/translations/locales/tr_TR.json b/src/deadline/client/ui/translations/locales/tr_TR.json index 7c981364e..cf8c1abd2 100644 --- a/src/deadline/client/ui/translations/locales/tr_TR.json +++ b/src/deadline/client/ui/translations/locales/tr_TR.json @@ -36,6 +36,7 @@ "Default farm": "Varsayılan farm", "Default queue": "Varsayılan kuyruk", "Default storage profile": "Varsayılan depolama profili", + "Storage profile": "Depolama profili", "Delete": "Sil", "Description": "Açıklama", "Do not ask again": "Bir daha sorma", diff --git a/src/deadline/client/ui/translations/locales/zh_CN.json b/src/deadline/client/ui/translations/locales/zh_CN.json index b65800c6e..d0b8c805a 100644 --- a/src/deadline/client/ui/translations/locales/zh_CN.json +++ b/src/deadline/client/ui/translations/locales/zh_CN.json @@ -36,6 +36,7 @@ "Default farm": "默认服务器农场", "Default queue": "默认队列", "Default storage profile": "默认存储配置文件", + "Storage profile": "存储配置文件", "Delete": "删除", "Description": "描述", "Do not ask again": "不再询问", diff --git a/src/deadline/client/ui/translations/locales/zh_TW.json b/src/deadline/client/ui/translations/locales/zh_TW.json index bfac49f9c..84a1155ef 100644 --- a/src/deadline/client/ui/translations/locales/zh_TW.json +++ b/src/deadline/client/ui/translations/locales/zh_TW.json @@ -36,6 +36,7 @@ "Default farm": "預設伺服器陣列", "Default queue": "預設佇列", "Default storage profile": "預設儲存設定檔", + "Storage profile": "儲存設定檔", "Delete": "刪除", "Description": "描述", "Do not ask again": "不要再詢問", diff --git a/src/deadline/client/ui/widgets/shared_job_settings_tab.py b/src/deadline/client/ui/widgets/shared_job_settings_tab.py index a77824de3..c3f871822 100644 --- a/src/deadline/client/ui/widgets/shared_job_settings_tab.py +++ b/src/deadline/client/ui/widgets/shared_job_settings_tab.py @@ -506,7 +506,7 @@ def _build_ui(self): self.queue_box = DeadlineQueueListComboBox(parent=self) self.layout.addRow(self.queue_box_label, self.queue_box) - self.storage_profile_box_label = QLabel(tr("Default storage profile")) + self.storage_profile_box_label = QLabel(tr("Storage profile")) self.storage_profile_box = DeadlineStorageProfileNameListComboBox(parent=self) self.layout.addRow(self.storage_profile_box_label, self.storage_profile_box) From 9137dd75e2c4b7ea0d6ea6d53ba903c2a8fa76b9 Mon Sep 17 00:00:00 2001 From: Louise Fox <208544511+folouiseAWS@users.noreply.github.com> Date: Tue, 10 Feb 2026 17:02:48 -0800 Subject: [PATCH 05/12] feat!: move farm/queue/storage profile from Settings Dialog to Submit Dialog Move farm, queue, and storage profile selection from Settings Dialog to Submit Dialog for a more streamlined workflow. Changes: - Add DeadlineCloudSettingsWidget to shared_job_settings_tab.py - Add deadline_cloud_resource_combo_boxes.py with reusable combo boxes - Remove farm/queue/storage profile from deadline_config_dialog.py - Update Squish tests for new UI layout - Add new tst_verify_submitter_deadline_cloud_settings test - Fix mypy errors in job_bundle_settings_tab.py BREAKING CHANGES: Settings Dialog no longer contains farm, queue, or storage profile dropdowns - these are now in the Submit Dialog. Signed-off-by: Louise Fox <208544511+folouiseAWS@users.noreply.github.com> --- requirements-testing.txt | 2 + .../ui/dialogs/deadline_config_dialog.py | 298 +----------------- .../ui/dialogs/submit_job_progress_dialog.py | 2 +- .../dialogs/submit_job_to_deadline_dialog.py | 2 +- .../client/ui/job_bundle_submitter.py | 4 +- src/deadline/client/ui/widgets/__init__.py | 2 +- .../deadline_authentication_status_widget.py | 7 +- .../deadline_cloud_resource_combo_boxes.py | 244 ++++++++++++++ .../ui/widgets/job_bundle_settings_tab.py | 11 +- .../ui/widgets/openjd_parameters_widget.py | 13 +- .../ui/widgets/shared_job_settings_tab.py | 109 +++---- test/squish/suite_deadline_gui/config.xml | 3 +- test/squish/suite_deadline_gui/envvars | 4 +- .../scripts/choose_jobbundledir_helpers.py | 12 +- .../shared/scripts/config.py | 10 +- .../shared/scripts/gui_submitter_helpers.py | 163 ++++++++++ .../shared/scripts/gui_submitter_locators.py | 55 ++-- .../shared/scripts/names.py | 61 +--- .../scripts/workstation_config_helpers.py | 132 -------- .../scripts/workstation_config_locators.py | 61 +--- test/squish/suite_deadline_gui/suite.conf | 3 +- .../tst_verify_gui_submitter_bundles/test.py | 2 +- .../tst_verify_settings_dialogue/test.py | 28 -- .../test.py | 131 ++++++++ .../ui/dialogs/test_help_dialog.py | 5 +- .../test_submit_job_to_deadline_dialog.py | 16 +- .../ui/widgets/test_host_requirements_tab.py | 44 ++- .../widgets/test_shared_job_settings_tab.py | 193 +++++++----- 28 files changed, 834 insertions(+), 783 deletions(-) create mode 100644 src/deadline/client/ui/widgets/deadline_cloud_resource_combo_boxes.py create mode 100644 test/squish/suite_deadline_gui/tst_verify_submitter_deadline_cloud_settings/test.py diff --git a/requirements-testing.txt b/requirements-testing.txt index c19271674..ee46b430d 100644 --- a/requirements-testing.txt +++ b/requirements-testing.txt @@ -17,6 +17,8 @@ ruff == 0.14.* moto == 5.* jsondiff == 2.* pyinstrument == 5.* +pytest-qt == 4.* +PySide6-essentials >= 6.6, < 6.11 # MCP (Model Context Protocol) library for MCP unit tests mcp >= 1.13.0; python_version >= '3.10' # OpenJD model library for job template parsing in mock backend diff --git a/src/deadline/client/ui/dialogs/deadline_config_dialog.py b/src/deadline/client/ui/dialogs/deadline_config_dialog.py index 33015f079..067f0078d 100644 --- a/src/deadline/client/ui/dialogs/deadline_config_dialog.py +++ b/src/deadline/client/ui/dialogs/deadline_config_dialog.py @@ -10,8 +10,7 @@ __all__ = ["DeadlineConfigDialog"] -import sys -import threading +import os from configparser import ConfigParser from logging import getLogger, root from typing import Callable, Dict, List, Optional @@ -42,12 +41,10 @@ QScrollArea, ) -import os - from ... import api from ..deadline_authentication_status import DeadlineAuthenticationStatus from ...config import config_file, get_setting_default, str2bool -from .._utils import CancelationFlag, block_signals, tr +from .._utils import block_signals, tr from ..widgets import DirectoryPickerWidget from ..widgets.deadline_authentication_status_widget import DeadlineAuthenticationStatusWidget from .deadline_login_dialog import DeadlineLoginDialog @@ -210,12 +207,6 @@ class DeadlineWorkstationConfigWidget(QWidget): # Signal for when the GUI is refreshed refreshed = Signal() - # Emitted when an async refresh_queues_list thread completes, - # provides (aws_profile_name, farm_id, [(queue_id, queue_name), ...]) - _queue_list_update = Signal(str, str, list) - # Emitted when an async refresh_storage_profiles_name_list thread completes, - # provides (aws_profile_name, farm_id, queue_id, [storage_profile_id, ...]) - _storage_profile_list_update = Signal(str, str, list) # This signal is sent when any background refresh thread catches an exception, # provides (operation_name, BaseException) _background_exception = Signal(str, BaseException) @@ -320,33 +311,9 @@ def _build_profile_settings_ui(self, group, layout): layout.addRow(job_history_dir_label, self.job_history_dir_edit) self.job_history_dir_edit.path_changed.connect(self.job_history_dir_changed) - self.default_farm_box = DeadlineFarmListComboBox(parent=group) - default_farm_box_label = self.labels["defaults.farm_id"] = QLabel(tr("Default farm")) - self.default_farm_box.box.currentIndexChanged.connect(self.default_farm_changed) - self.default_farm_box.background_exception.connect(self.handle_background_exception) - layout.addRow(default_farm_box_label, self.default_farm_box) - def _build_farm_settings_ui(self, group, layout): layout.setFieldGrowthPolicy(QFormLayout.ExpandingFieldsGrow) - self.default_queue_box = DeadlineQueueListComboBox(parent=group) - default_queue_box_label = self.labels["defaults.queue_id"] = QLabel(tr("Default queue")) - self.default_queue_box.box.currentIndexChanged.connect(self.default_queue_changed) - self.default_queue_box.background_exception.connect(self.handle_background_exception) - layout.addRow(default_queue_box_label, self.default_queue_box) - - self.default_storage_profile_box = DeadlineStorageProfileNameListComboBox(parent=group) - default_storage_profile_box_label = self.labels["settings.storage_profile_id"] = QLabel( - tr("Default storage profile") - ) - self.default_storage_profile_box.box.currentIndexChanged.connect( - self.default_storage_profile_name_changed - ) - self.default_storage_profile_box.background_exception.connect( - self.handle_background_exception - ) - layout.addRow(default_storage_profile_box_label, self.default_storage_profile_box) - item_name_copied = JobAttachmentsFileSystem.COPIED.value item_name_virtual = JobAttachmentsFileSystem.VIRTUAL.value job_attachments_file_system_tooltip = ( @@ -717,10 +684,9 @@ def _fill_aws_profiles_box(self): self.aws_profiles_box.addItems(list(self.aws_profile_names)) def refresh_lists(self): - if api.check_deadline_api_available(): - self.default_farm_box.refresh_list() - self.default_queue_box.refresh_list() - self.default_storage_profile_box.refresh_list() + # Farm, queue, and storage profile lists are now managed by + # DeadlineCloudSettingsWidget in the Submit Dialog, not here. + pass def refresh(self): """ @@ -731,9 +697,6 @@ def refresh(self): self.config.read_dict(config_file.read_config()) for setting_name, value in self.changes.items(): config_file.set_setting(setting_name, value, self.config) - self.default_farm_box.set_config(self.config) - self.default_queue_box.set_config(self.config) - self.default_storage_profile_box.set_config(self.config) with block_signals(self.aws_profiles_box): aws_profile_name = config_file.get_setting( @@ -757,14 +720,9 @@ def refresh(self): ) self.job_history_dir_edit.setText(job_history_dir) - self.default_farm_box.refresh_selected_id() - for refresh_callback in self._refresh_callbacks: refresh_callback() - self.default_queue_box.refresh_selected_id() - self.default_storage_profile_box.refresh_selected_id() - # Put an orange box around the labels for any settings that are changed for setting_name, label in self.labels.items(): if setting_name in self.changes: @@ -781,11 +739,6 @@ def apply(self) -> bool: Returns True if the settings were applied, False otherwise. """ - # We need to retrieve here as changing Queue's won't update. - self.changes["settings.storage_profile_id"] = ( - self.default_storage_profile_box.box.currentData() - ) - for setting_name, value in self.changes.items(): if value.startswith(NOT_VALID_MARKER): QMessageBox.warning( # type: ignore[call-arg] @@ -818,9 +771,6 @@ def apply(self) -> bool: def aws_profile_changed(self, value): self.changes["defaults.aws_profile_name"] = value - self.default_farm_box.clear_list() - self.default_queue_box.clear_list() - self.default_storage_profile_box.clear_list() self.refresh() def job_history_dir_changed(self): @@ -832,23 +782,6 @@ def job_history_dir_changed(self): self.changes["settings.job_history_dir"] = job_history_dir self.refresh() - def default_farm_changed(self, index): - self.changes["defaults.farm_id"] = self.default_farm_box.box.itemData(index) - self.refresh() - self.default_queue_box.refresh_list() - self.default_storage_profile_box.refresh_list() - - def default_queue_changed(self, index): - self.changes["defaults.queue_id"] = self.default_queue_box.box.itemData(index) - self.refresh() - self.default_storage_profile_box.refresh_list() - - def default_storage_profile_name_changed(self, index): - self.changes["settings.storage_profile_id"] = self.default_storage_profile_box.box.itemData( - index - ) - self.refresh() - def _on_add_known_path(self): """Handle adding a new known path""" path = QFileDialog.getExistingDirectory( @@ -920,224 +853,3 @@ def _on_edit_known_path(self): self.changes["settings.known_asset_paths"] = os.pathsep.join(current_paths) self.refresh() - - -class _DeadlineResourceListComboBox(QWidget): - """ - A ComboBox for selecting an AWS Deadline Cloud Id, with a refresh button. - - The caller should connect the `background_exception` signal, e.g. - to show a message box, and should call `set_config` whenever there is - a change to the AWS Deadline Cloud config object. - - Args: - resource_name (str): The resource name for the list, like "Farm", - "Queue", "Fleet". - """ - - # Emitted when the background refresh thread catches an exception, - # provides (operation_name, BaseException) - background_exception = Signal(str, BaseException) - - # Emitted when an async refresh_farms_list thread completes, - # provides (refresh_id, [(farm_id, farm_name), ...]) - _list_update = Signal(int, list) - - def __init__(self, resource_name, setting_name, parent: Optional[QWidget] = None): - super().__init__(parent) - - self.__refresh_thread = None - self.__refresh_id = 0 - self.canceled = CancelationFlag() - self.destroyed.connect(self.canceled.set_canceled) - - self.resource_name = resource_name - self.setting_name = setting_name - - self._build_ui() - - def _build_ui(self): - self.box = QComboBox(parent=self) - layout = QHBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(self.box, stretch=1) - - self.refresh_button = QPushButton("") - layout.addWidget(self.refresh_button) - self.refresh_button.setIcon(QApplication.style().standardIcon(QStyle.SP_BrowserReload)) - self.refresh_button.setFixedSize(QSize(22, 22)) # Make the button square - self.refresh_button.clicked.connect(self.refresh_list) - self._list_update.connect(self.handle_list_update) - self.background_exception.connect(self.handle_background_exception) - - def handle_background_exception(self, e): - with block_signals(self.box): - self.box.clear() - self.refresh_selected_id() - - def count(self) -> int: - """Returns the number of items in the combobox""" - return self.box.count() - - def set_config(self, config: ConfigParser) -> None: - """Updates the AWS Deadline Cloud config object the control uses.""" - self.config = config - - def clear_list(self): - """ - Fully clears the list. The caller needs to call either - `refresh_list` or `refresh_selected_id` at a later point - to finish it. - """ - with block_signals(self.box): - self.box.clear() - - def refresh_list(self): - """ - Starts a background thread to refresh the resource list. - """ - config = self.config - selected_id = config_file.get_setting(self.setting_name, config=config) - # Reset to a list of just the currently configured id during refresh - with block_signals(self.box): - self.box.clear() - self.box.addItem("", userData=selected_id) - - self.__refresh_id += 1 - self.__refresh_thread = threading.Thread( - target=self._refresh_thread_function, - name=f"AWS Deadline Cloud refresh {self.resource_name} thread", - args=(self.__refresh_id, config), - ) - self.__refresh_thread.start() - - def handle_list_update(self, refresh_id, items_list): - # Apply the refresh if it's still for the latest call - if refresh_id == self.__refresh_id: - with block_signals(self.box): - self.box.clear() - for name, id in items_list: - self.box.addItem(name, userData=id) - - self.refresh_selected_id() - - def refresh_selected_id(self): - """Refreshes the selected id from the config object""" - selected_id = config_file.get_setting(self.setting_name, config=self.config) - # Restore the selected Id, inserting a new item if - # it doesn't exist in the list. - with block_signals(self.box): - index = self.box.findData(selected_id) - if index >= 0: - self.box.setCurrentIndex(index) - elif selected_id: - # User has a configured ID but it's not in the list. This happens when - # the user has permission to use a resource (e.g., queue) but lacks - # permission to list resources (e.g., ListFarms). Show the raw ID so - # they can still see their configured resource. - self.box.insertItem(0, selected_id, userData=selected_id) - self.box.setCurrentIndex(0) - else: - # No ID selected - index = self.box.findText("") - if index >= 0: - self.box.setCurrentIndex(index) - else: - self.box.insertItem(0, "", userData="") - self.box.setCurrentIndex(0) - - def _refresh_thread_function(self, refresh_id: int, config: Optional[ConfigParser] = None): - """ - This function gets started in a background thread to refresh the list. - """ - try: - resources = self.list_resources(config=config) - if not self.canceled: - self._list_update.emit(refresh_id, resources) - except BaseException as e: - if not self.canceled and refresh_id == self.__refresh_id: - self.background_exception.emit(f"Refresh {self.resource_name}s list", e) - - -class DeadlineFarmListComboBox(_DeadlineResourceListComboBox): - def __init__(self, parent: Optional[QWidget] = None): - super().__init__(resource_name="Farm", setting_name="defaults.farm_id", parent=parent) - - def list_resources(self, config: Optional[ConfigParser]): - response = api.list_farms(config=config) - return sorted( - [(item["displayName"], item["farmId"]) for item in response["farms"]], - key=lambda item: (item[0].casefold(), item[1]), - ) - - -class DeadlineQueueListComboBox(_DeadlineResourceListComboBox): - def __init__(self, parent: Optional[QWidget] = None): - super().__init__(resource_name="Queue", setting_name="defaults.queue_id", parent=parent) - - def list_resources(self, config: Optional[ConfigParser]): - default_farm_id = config_file.get_setting("defaults.farm_id", config=config) - if default_farm_id: - response = api.list_queues(config=config, farmId=default_farm_id) - return sorted( - [(item["displayName"], item["queueId"]) for item in response["queues"]], - key=lambda item: (item[0].casefold(), item[1]), - ) - else: - return [] - - -class DeadlineStorageProfileNameListComboBox(_DeadlineResourceListComboBox): - WINDOWS_OS = "windows" - MAC_OS = "macos" - LINUX_OS = "linux" - - def __init__(self, parent: Optional[QWidget] = None): - super().__init__( - resource_name="Storage profile", - setting_name="settings.storage_profile_id", - parent=parent, - ) - - def list_resources(self, config: Optional[ConfigParser]): - default_farm_id = config_file.get_setting("defaults.farm_id", config=config) - default_queue_id = config_file.get_setting("defaults.queue_id", config=config) - if default_farm_id and default_queue_id: - response = api.list_storage_profiles_for_queue( - config=config, farmId=default_farm_id, queueId=default_queue_id - ) - storage_profiles = response.get("storageProfiles", []) - # add a "" option since its possible to select nothing for this type - # of resource - storage_profiles.append( - { - "storageProfileId": "", - "displayName": "", - "osFamily": self._get_current_os(), - } - ) - return sorted( - [ - (item["displayName"], item["storageProfileId"]) - for item in storage_profiles - if self._get_current_os() == item["osFamily"].lower() - ], - key=lambda item: (item[0].casefold(), item[1]), - ) - else: - return [] - - def _get_current_os(self) -> str: - """ - Get a string specifying what the OS is, following the format the Deadline storage profile API expects. - """ - if sys.platform.startswith("linux"): - return self.LINUX_OS - - if sys.platform.startswith("darwin"): - return self.MAC_OS - - if sys.platform.startswith("win"): - return self.WINDOWS_OS - - return "Unknown" diff --git a/src/deadline/client/ui/dialogs/submit_job_progress_dialog.py b/src/deadline/client/ui/dialogs/submit_job_progress_dialog.py index 14d9ac1f0..1580b1b9d 100644 --- a/src/deadline/client/ui/dialogs/submit_job_progress_dialog.py +++ b/src/deadline/client/ui/dialogs/submit_job_progress_dialog.py @@ -127,7 +127,7 @@ class SubmitJobProgressDialog(QDialog): # This signal is sent when the background thread succeeds. submission_thread_succeeded = Signal(str) - progress_window_closed = Signal(None) + progress_window_closed = Signal() job_id: Optional[str] = None diff --git a/src/deadline/client/ui/dialogs/submit_job_to_deadline_dialog.py b/src/deadline/client/ui/dialogs/submit_job_to_deadline_dialog.py index 939b894d3..dbfa7d576 100644 --- a/src/deadline/client/ui/dialogs/submit_job_to_deadline_dialog.py +++ b/src/deadline/client/ui/dialogs/submit_job_to_deadline_dialog.py @@ -113,7 +113,7 @@ def __init__( attachments: AssetReferences, on_create_job_bundle_callback: OnCreateJobBundleCallback, parent: Optional[QWidget] = None, - f: Qt.WindowFlags = Qt.WindowFlags(), + f=Qt.WindowFlags(), show_host_requirements_tab: bool = False, host_requirements: Optional[HostRequirements] = None, submitter_info: Optional[SubmitterInfo] = None, diff --git a/src/deadline/client/ui/job_bundle_submitter.py b/src/deadline/client/ui/job_bundle_submitter.py index b2cdc60f2..4834b9a1d 100644 --- a/src/deadline/client/ui/job_bundle_submitter.py +++ b/src/deadline/client/ui/job_bundle_submitter.py @@ -152,8 +152,8 @@ def show_job_bundle_submitter( app = QApplication.instance() main_windows = [ widget - for widget in app.topLevelWidgets() - if isinstance(widget, QMainWindow) # type: ignore[union-attr] + for widget in (app.topLevelWidgets() if app else []) + if isinstance(widget, QMainWindow) ] if main_windows: parent = main_windows[0] diff --git a/src/deadline/client/ui/widgets/__init__.py b/src/deadline/client/ui/widgets/__init__.py index d32d6f432..ae8b06358 100644 --- a/src/deadline/client/ui/widgets/__init__.py +++ b/src/deadline/client/ui/widgets/__init__.py @@ -38,7 +38,7 @@ from .openjd_parameters_widget import OpenJDParametersWidget from .path_widgets import DirectoryPickerWidget, InputFilePickerWidget, OutputFilePickerWidget from .shared_job_settings_tab import ( - DeadlineCloudSettingsWidget, SharedJobSettingsWidget, SharedJobPropertiesWidget, + DeadlineCloudSettingsWidget, ) diff --git a/src/deadline/client/ui/widgets/deadline_authentication_status_widget.py b/src/deadline/client/ui/widgets/deadline_authentication_status_widget.py index 613a3deb8..27e4f7c98 100644 --- a/src/deadline/client/ui/widgets/deadline_authentication_status_widget.py +++ b/src/deadline/client/ui/widgets/deadline_authentication_status_widget.py @@ -44,8 +44,9 @@ def showEvent(self, event): """ Override showEvent to position the menu to the right of the parent button. """ - if self.parent(): - parent_top_right = self.parent().mapToGlobal(self.parent().rect().topRight()) + parent_widget = self.parent() + if isinstance(parent_widget, QWidget): + parent_top_right = parent_widget.mapToGlobal(parent_widget.rect().topRight()) self.move(parent_top_right.x(), parent_top_right.y()) super().showEvent(event) @@ -403,4 +404,4 @@ def _apply_ui_state(self, config: AuthenticationStateConfig) -> None: if any(action.isVisible() for action in self._auth_menu.actions()): self._profile_button.setMenu(self._auth_menu) else: - self._profile_button.setMenu(None) + self._profile_button.setMenu(None) # type: ignore[arg-type] diff --git a/src/deadline/client/ui/widgets/deadline_cloud_resource_combo_boxes.py b/src/deadline/client/ui/widgets/deadline_cloud_resource_combo_boxes.py new file mode 100644 index 000000000..ea77eb817 --- /dev/null +++ b/src/deadline/client/ui/widgets/deadline_cloud_resource_combo_boxes.py @@ -0,0 +1,244 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +""" +Combo box widgets for selecting AWS Deadline Cloud resources (farms, queues, storage profiles). +These are used by both the Settings Dialog and the Submit Dialog. +""" + +__all__ = [ + "DeadlineFarmListComboBox", + "DeadlineQueueListComboBox", + "DeadlineStorageProfileNameListComboBox", +] + +import sys +import threading +from configparser import ConfigParser +from typing import Optional + +from qtpy.QtCore import QSize, Signal +from qtpy.QtWidgets import ( # type: ignore + QApplication, + QComboBox, + QHBoxLayout, + QPushButton, + QStyle, + QWidget, +) + +from ... import api +from ...config import config_file +from .._utils import CancelationFlag, block_signals + + +class _DeadlineResourceListComboBox(QWidget): + """ + A ComboBox for selecting an AWS Deadline Cloud Id, with a refresh button. + + The caller should connect the `background_exception` signal, e.g. + to show a message box, and should call `set_config` whenever there is + a change to the AWS Deadline Cloud config object. + + Args: + resource_name (str): The resource name for the list, like "Farm", + "Queue", "Fleet". + """ + + # Emitted when the background refresh thread catches an exception, + # provides (operation_name, BaseException) + background_exception = Signal(str, BaseException) + + # Emitted when an async refresh_farms_list thread completes, + # provides (refresh_id, [(farm_id, farm_name), ...]) + _list_update = Signal(int, list) + + def __init__(self, resource_name, setting_name, parent: Optional[QWidget] = None): + super().__init__(parent) + + self.__refresh_thread = None + self.__refresh_id = 0 + self.canceled = CancelationFlag() + self.destroyed.connect(self.canceled.set_canceled) + + self.resource_name = resource_name + self.setting_name = setting_name + + self._build_ui() + + def _build_ui(self): + self.box = QComboBox(parent=self) + layout = QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(self.box, stretch=1) + + self.refresh_button = QPushButton("") + layout.addWidget(self.refresh_button) + self.refresh_button.setIcon(QApplication.style().standardIcon(QStyle.SP_BrowserReload)) + self.refresh_button.setFixedSize(QSize(22, 22)) # Make the button square + self.refresh_button.clicked.connect(self.refresh_list) + self._list_update.connect(self.handle_list_update) + self.background_exception.connect(self.handle_background_exception) + + def handle_background_exception(self, e): + with block_signals(self.box): + self.box.clear() + self.refresh_selected_id() + + def count(self) -> int: + """Returns the number of items in the combobox""" + return self.box.count() + + def set_config(self, config: ConfigParser) -> None: + """Updates the AWS Deadline Cloud config object the control uses.""" + self.config = config + + def clear_list(self): + """ + Fully clears the list. The caller needs to call either + `refresh_list` or `refresh_selected_id` at a later point + to finish it. + """ + with block_signals(self.box): + self.box.clear() + + def refresh_list(self): + """ + Starts a background thread to refresh the resource list. + """ + config = self.config + selected_id = config_file.get_setting(self.setting_name, config=config) + # Reset to a list of just the currently configured id during refresh + with block_signals(self.box): + self.box.clear() + self.box.addItem("", userData=selected_id) + + self.__refresh_id += 1 + self.__refresh_thread = threading.Thread( + target=self._refresh_thread_function, + name=f"AWS Deadline Cloud refresh {self.resource_name} thread", + args=(self.__refresh_id, config), + ) + self.__refresh_thread.start() + + def handle_list_update(self, refresh_id, items_list): + # Apply the refresh if it's still for the latest call + if refresh_id == self.__refresh_id: + with block_signals(self.box): + self.box.clear() + for name, id in items_list: + self.box.addItem(name, userData=id) + + self.refresh_selected_id() + + def refresh_selected_id(self): + """Refreshes the selected id from the config object""" + selected_id = config_file.get_setting(self.setting_name, config=self.config) + # Restore the selected Id, inserting a new item if + # it doesn't exist in the list. + with block_signals(self.box): + index = self.box.findData(selected_id) + if index >= 0: + self.box.setCurrentIndex(index) + else: + # Some cases allow to select "nothing" and insert an item to indicate such + index = self.box.findText("") + if index >= 0: + self.box.setCurrentIndex(index) + else: + self.box.insertItem(0, "", userData="") + self.box.setCurrentIndex(0) + + def _refresh_thread_function(self, refresh_id: int, config: Optional[ConfigParser] = None): + """ + This function gets started in a background thread to refresh the list. + """ + try: + resources = self.list_resources(config=config) + if not self.canceled: + self._list_update.emit(refresh_id, resources) + except BaseException as e: + if not self.canceled and refresh_id == self.__refresh_id: + self.background_exception.emit(f"Refresh {self.resource_name}s list", e) + + +class DeadlineFarmListComboBox(_DeadlineResourceListComboBox): + def __init__(self, parent: Optional[QWidget] = None): + super().__init__(resource_name="Farm", setting_name="defaults.farm_id", parent=parent) + + def list_resources(self, config: Optional[ConfigParser]): + response = api.list_farms(config=config) + return sorted( + [(item["displayName"], item["farmId"]) for item in response["farms"]], + key=lambda item: (item[0].casefold(), item[1]), + ) + + +class DeadlineQueueListComboBox(_DeadlineResourceListComboBox): + def __init__(self, parent: Optional[QWidget] = None): + super().__init__(resource_name="Queue", setting_name="defaults.queue_id", parent=parent) + + def list_resources(self, config: Optional[ConfigParser]): + default_farm_id = config_file.get_setting("defaults.farm_id", config=config) + if default_farm_id: + response = api.list_queues(config=config, farmId=default_farm_id) + return sorted( + [(item["displayName"], item["queueId"]) for item in response["queues"]], + key=lambda item: (item[0].casefold(), item[1]), + ) + else: + return [] + + +class DeadlineStorageProfileNameListComboBox(_DeadlineResourceListComboBox): + WINDOWS_OS = "windows" + MAC_OS = "macos" + LINUX_OS = "linux" + + def __init__(self, parent: Optional[QWidget] = None): + super().__init__( + resource_name="Storage profile", + setting_name="settings.storage_profile_id", + parent=parent, + ) + + def list_resources(self, config: Optional[ConfigParser]): + default_farm_id = config_file.get_setting("defaults.farm_id", config=config) + default_queue_id = config_file.get_setting("defaults.queue_id", config=config) + if default_farm_id and default_queue_id: + response = api.list_storage_profiles_for_queue( + config=config, farmId=default_farm_id, queueId=default_queue_id + ) + storage_profiles = response.get("storageProfiles", []) + # add a "" option since its possible to select nothing for this type + # of resource + storage_profiles.append( + { + "storageProfileId": "", + "displayName": "", + "osFamily": self._get_current_os(), + } + ) + return sorted( + [ + (item["displayName"], item["storageProfileId"]) + for item in storage_profiles + if self._get_current_os() == item["osFamily"].lower() + ], + key=lambda item: (item[0].casefold(), item[1]), + ) + else: + return [] + + def _get_current_os(self) -> str: + """ + Get a string specifying what the OS is, following the format the Deadline storage profile API expects. + """ + if sys.platform.startswith("linux"): + return self.LINUX_OS + + if sys.platform.startswith("darwin"): + return self.MAC_OS + + if sys.platform.startswith("win"): + return self.WINDOWS_OS + + return "Unknown" diff --git a/src/deadline/client/ui/widgets/job_bundle_settings_tab.py b/src/deadline/client/ui/widgets/job_bundle_settings_tab.py index 93cba618e..6192c736e 100644 --- a/src/deadline/client/ui/widgets/job_bundle_settings_tab.py +++ b/src/deadline/client/ui/widgets/job_bundle_settings_tab.py @@ -46,7 +46,7 @@ class JobBundleSettingsWidget(QWidget): def __init__(self, initial_settings: JobBundleSettings, parent: Optional[QWidget] = None): super().__init__(parent=parent) - self.parent = parent + self._parent_widget = parent self.param_layout = QVBoxLayout() @@ -64,7 +64,10 @@ def refresh_ui(self, settings: JobBundleSettings): # Clear the layout for i in reversed(range(self.param_layout.count())): item = self.param_layout.takeAt(i) - item.widget().deleteLater() + if item is not None: + widget = item.widget() + if widget is not None: + widget.deleteLater() self.parameters_widget = OpenJDParametersWidget( parameter_definitions=settings.parameters, parent=self @@ -114,8 +117,8 @@ def on_load_bundle(self): logger.warning(msg) return - if self.parent and hasattr(self.parent, "refresh"): - self.parent.refresh( + if self._parent_widget is not None and hasattr(self._parent_widget, "refresh"): + self._parent_widget.refresh( job_settings=job_settings, auto_detected_attachments=asset_references, attachments=None, diff --git a/src/deadline/client/ui/widgets/openjd_parameters_widget.py b/src/deadline/client/ui/widgets/openjd_parameters_widget.py index 33112f2a7..49db5bc14 100644 --- a/src/deadline/client/ui/widgets/openjd_parameters_widget.py +++ b/src/deadline/client/ui/widgets/openjd_parameters_widget.py @@ -90,10 +90,15 @@ def rebuild_ui( if isinstance(layout, QVBoxLayout): for index in reversed(range(layout.count())): child = layout.takeAt(index) - if child.widget(): - child.widget().deleteLater() - elif child.layout(): - child.layout().deleteLater() + if child is None: + continue + widget = child.widget() + if widget is not None: + widget.deleteLater() + else: + child_layout = child.layout() + if child_layout is not None: + child_layout.deleteLater() else: layout = QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) diff --git a/src/deadline/client/ui/widgets/shared_job_settings_tab.py b/src/deadline/client/ui/widgets/shared_job_settings_tab.py index c3f871822..61154b4ad 100644 --- a/src/deadline/client/ui/widgets/shared_job_settings_tab.py +++ b/src/deadline/client/ui/widgets/shared_job_settings_tab.py @@ -7,7 +7,7 @@ from __future__ import annotations import threading -from typing import Any, Dict, Optional +from typing import Any, Optional from qtpy.QtCore import Signal # type: ignore from qtpy.QtWidgets import ( # type: ignore @@ -26,6 +26,11 @@ from .._utils import CancelationFlag, tr from .openjd_parameters_widget import OpenJDParametersWidget from ...api import get_queue_parameter_definitions +from .deadline_cloud_resource_combo_boxes import ( + DeadlineFarmListComboBox, + DeadlineQueueListComboBox, + DeadlineStorageProfileNameListComboBox, +) class SharedJobSettingsWidget(QWidget): # pylint: disable=too-few-public-methods @@ -471,33 +476,21 @@ def update_settings(self, settings): class DeadlineCloudSettingsWidget(QGroupBox): """ - UI component for the Deadline Cloud settings. + UI component for the Deadline Cloud settings (farm, queue, storage profile). + Used in the Submit Dialog's "Shared job settings" tab. """ def __init__(self, *, parent: Optional[QWidget] = None): super().__init__(tr("Deadline Cloud settings"), parent=parent) - self.deadline_settings: Dict[str, Any] = {"counter": -1} self.layout = QFormLayout(self) self.layout.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow) self._build_ui() - def _set_enabled_with_label(self, prop_name: str, enabled: bool): - """Sets the enabled status of a control and its label""" - getattr(self, prop_name).setEnabled(enabled) - getattr(self, prop_name + "_label").setEnabled(enabled) - def _build_ui(self): """ Build the UI for the Deadline settings """ - # Import here to avoid circular import - from ..dialogs.deadline_config_dialog import ( - DeadlineFarmListComboBox, - DeadlineQueueListComboBox, - DeadlineStorageProfileNameListComboBox, - ) - self.farm_box_label = QLabel(tr("Farm")) self.farm_box = DeadlineFarmListComboBox(parent=self) self.layout.addRow(self.farm_box_label, self.farm_box) @@ -546,38 +539,33 @@ def _set_storage_profile_visible(self, visible: bool): def _update_storage_profile_visibility(self): """Update storage profile visibility based on available profiles""" - # Check if there are actual storage profiles (not just placeholder items) - # Note: "" sorts first alphabetically and has empty string as data, - # so we need to check if there are items with non-empty data - count = self.storage_profile_box.box.count() - has_real_profiles = False - for i in range(count): - item_data = self.storage_profile_box.box.itemData(i) - item_text = self.storage_profile_box.box.itemText(i) - # Skip placeholder items - if item_text in ("", "") or item_data in (None, ""): - continue - has_real_profiles = True - break + box = self.storage_profile_box.box + has_real_profiles = any( + box.itemData(i) not in (None, "") + and box.itemText(i) not in ("", "") + for i in range(box.count()) + ) self._set_storage_profile_visible(has_real_profiles) + def _update_all_box_configs(self): + """Re-read config and update all combo boxes.""" + config = config_file.read_config() + self.farm_box.set_config(config) + self.queue_box.set_config(config) + self.storage_profile_box.set_config(config) + def _on_farm_changed(self, index: int): """Handle farm selection change in Submit Dialog""" if index < 0: return - # Get the selected farm ID from the combo box farm_id = self.farm_box.box.itemData(index) if farm_id is None: return - # Update config immediately (unlike Settings Dialog which defers to apply()) set_setting("defaults.farm_id", farm_id) - - # Refresh queue list for the new farm (same as Settings Dialog) + self._update_all_box_configs() self.queue_box.refresh_list() - - # Notify parent to refresh (triggers submit button state update and queue parameters) self._notify_parent_refresh() def _on_queue_changed(self, index: int): @@ -585,18 +573,13 @@ def _on_queue_changed(self, index: int): if index < 0: return - # Get the selected queue ID from the combo box queue_id = self.queue_box.box.itemData(index) if queue_id is None: return - # Update config immediately (unlike Settings Dialog which defers to apply()) set_setting("defaults.queue_id", queue_id) - - # Refresh storage profile list for the new queue + self._update_all_box_configs() self.storage_profile_box.refresh_list() - - # Notify parent to refresh (triggers submit button state update and queue parameters) self._notify_parent_refresh() def _on_storage_profile_changed(self, index: int): @@ -610,28 +593,28 @@ def _on_storage_profile_changed(self, index: int): # Update config immediately set_setting("settings.storage_profile_id", storage_profile_id if storage_profile_id else "") + def _find_parent_with_attr(self, attr_name: str) -> Optional[QWidget]: + """Find first parent widget with the given attribute.""" + parent: Optional[QWidget] = self.parent() # type: ignore[assignment] + while parent is not None: + if hasattr(parent, attr_name): + return parent + parent = ( + parent.parent() if hasattr(parent, "parent") and callable(parent.parent) else None + ) # type: ignore[assignment] + return None + def _notify_parent_refresh(self): """Helper to notify parent widgets to refresh after config changes""" - # Find SharedJobSettingsWidget parent to refresh queue parameters - parent_widget = self.parent() - while parent_widget is not None: - if hasattr(parent_widget, "refresh_queue_parameters"): - parent_widget.refresh_queue_parameters() - if hasattr(parent_widget, "parent") and callable(parent_widget.parent): - parent_widget = parent_widget.parent() - else: - break - - # Find SubmitJobToDeadlineDialog to refresh submit button state - parent_widget = self.parent() - while parent_widget is not None: - if hasattr(parent_widget, "refresh_deadline_settings"): - parent_widget.refresh_deadline_settings() - break - if hasattr(parent_widget, "parent") and callable(parent_widget.parent): - parent_widget = parent_widget.parent() - else: - break + # Find and call refresh_queue_parameters on parent chain + parent = self._find_parent_with_attr("refresh_queue_parameters") + if parent: + parent.refresh_queue_parameters() + + # Find and call refresh_deadline_settings on parent chain + parent = self._find_parent_with_attr("refresh_deadline_settings") + if parent: + parent.refresh_deadline_settings() def refresh_setting_controls(self, deadline_authorized): """ @@ -643,11 +626,7 @@ def refresh_setting_controls(self, deadline_authorized): api.check_deadline_available, for example from an AWS Deadline Cloud Status Widget. """ - # Update config for combo boxes - config = config_file.read_config() - self.farm_box.set_config(config) - self.queue_box.set_config(config) - self.storage_profile_box.set_config(config) + self._update_all_box_configs() # Refresh selected items to reflect current config self.farm_box.refresh_selected_id() diff --git a/test/squish/suite_deadline_gui/config.xml b/test/squish/suite_deadline_gui/config.xml index 326b954a5..2c3c3c99f 100644 --- a/test/squish/suite_deadline_gui/config.xml +++ b/test/squish/suite_deadline_gui/config.xml @@ -1,5 +1,4 @@ - - + diff --git a/test/squish/suite_deadline_gui/envvars b/test/squish/suite_deadline_gui/envvars index f1c12beac..63243ccd5 100644 --- a/test/squish/suite_deadline_gui/envvars +++ b/test/squish/suite_deadline_gui/envvars @@ -1,4 +1,2 @@ -LD_LIBRARY_PATH=/home/user/.local/lib/python3.9/site-packages/PySide6/Qt/lib/ -PATH=C:\Users\user\AppData\Local\Programs\Python\Python310\Lib\site-packages\PySide6\ SQUISH_NO_CAPTURE_OUTPUT=1 -DYLD_LIBRARY_PATH=/Users/user/Library/Python/3.9/lib/python/site-packages/PySide6/Qt/lib/ \ No newline at end of file +QT_API=pyside6 diff --git a/test/squish/suite_deadline_gui/shared/scripts/choose_jobbundledir_helpers.py b/test/squish/suite_deadline_gui/shared/scripts/choose_jobbundledir_helpers.py index 64bd029ee..9d7fb2250 100644 --- a/test/squish/suite_deadline_gui/shared/scripts/choose_jobbundledir_helpers.py +++ b/test/squish/suite_deadline_gui/shared/scripts/choose_jobbundledir_helpers.py @@ -74,17 +74,17 @@ def load_different_job_bundle(): str( squish.waitForObjectExists(gui_submitter_locators.load_different_job_bundle_button).text ), - "Load a different job bundle", - "Expect Load a different job bundle button to contain correct text.", + "Load Bundle", + "Expect Load Bundle button to contain correct text.", ) - # verify load a different job bundle button is enabled + # verify load bundle button is enabled test.compare( squish.waitForObjectExists(gui_submitter_locators.load_different_job_bundle_button).enabled, True, - "Expect Load a different job bundle button to be enabled.", + "Expect Load Bundle button to be enabled.", ) - # click on load a different job bundle button - test.log("Hitting `Load a different job bundle` button.") + # click on load bundle button + test.log("Hitting `Load Bundle` button.") squish.clickButton( squish.waitForObject(gui_submitter_locators.load_different_job_bundle_button) ) diff --git a/test/squish/suite_deadline_gui/shared/scripts/config.py b/test/squish/suite_deadline_gui/shared/scripts/config.py index 9385bee64..b8ba2e016 100644 --- a/test/squish/suite_deadline_gui/shared/scripts/config.py +++ b/test/squish/suite_deadline_gui/shared/scripts/config.py @@ -7,6 +7,10 @@ # get user's home directory home_dir = str(Path.home()) +# derive repo root from this file's location +# config.py is at: /test/squish/suite_deadline_gui/shared/scripts/config.py +_repo_root = str(Path(__file__).resolve().parents[5]) + # get python version for Deadline executable file path python_version = f"Python{sys.version_info.major}{sys.version_info.minor}" # Deadline executable file path on Windows @@ -45,10 +49,8 @@ # tst_verify_gui_submitter_bundles test suite: # Simple UI with Job Attachments (simple_ui_with_ja) -simple_ui_with_ja = ( - f"{home_dir}/deadline-cloud/test/squish/deadline_gui_test_samples/simple_ui_with_ja" -) +simple_ui_with_ja = f"{_repo_root}/test/squish/deadline_gui_test_samples/simple_ui_with_ja" simple_ui_with_ja_name = "Simple UI with Job Attachments" # Simple UI - No Job Attachments (simple_ui_no_ja) -simple_ui_no_ja = f"{home_dir}/deadline-cloud/test/squish/deadline_gui_test_samples/simple_ui_no_ja" +simple_ui_no_ja = f"{_repo_root}/test/squish/deadline_gui_test_samples/simple_ui_no_ja" simple_ui_no_ja_name = "Simple UI - No Job Attachments" diff --git a/test/squish/suite_deadline_gui/shared/scripts/gui_submitter_helpers.py b/test/squish/suite_deadline_gui/shared/scripts/gui_submitter_helpers.py index fb7e49e49..740b910a9 100644 --- a/test/squish/suite_deadline_gui/shared/scripts/gui_submitter_helpers.py +++ b/test/squish/suite_deadline_gui/shared/scripts/gui_submitter_helpers.py @@ -3,6 +3,7 @@ # mypy: disable-error-code="attr-defined" import gui_submitter_locators +import platform import squish import test @@ -51,3 +52,165 @@ def verify_shared_job_settings( job_name, "Expect correct job bundle job name to be displayed by default.", ) + + +def _wait_for_combo_box_value(locator, expected_value: str, timeout_ms: int = 5000): + """Wait for a combo box to show the expected value (handles async refresh).""" + import time + + start_time = time.time() + timeout_sec = timeout_ms / 1000.0 + while time.time() - start_time < timeout_sec: + try: + current_text = str(squish.waitForObjectExists(locator).currentText) + if current_text == expected_value: + return True + if current_text != "": + # Value stabilized but doesn't match - fail fast + return False + except Exception: + pass + time.sleep(0.1) + return False + + +def set_farm_name(farm_name: str): + """Set the farm in the Submit Dialog's Deadline Cloud settings.""" + # Ensure we're on the Shared job settings tab + navigate_shared_job_settings() + # Open farm dropdown menu + squish.mouseClick( + squish.waitForObject(gui_submitter_locators.deadline_cloud_settings_farm_dropdown) + ) + test.log("Opened farm name drop down menu in Submit Dialog.") + test.compare( + squish.waitForObjectExists( + gui_submitter_locators.farm_name_dropdown_locator(farm_name) + ).text, + farm_name, + "Expect farm name to be present in drop down.", + ) + # Select the farm + squish.mouseClick( + squish.waitForObjectItem( + gui_submitter_locators.deadline_cloud_settings_farm_dropdown, farm_name + ) + ) + test.log(f"Selected farm name: {farm_name}") + # Wait for async refresh to complete + test.verify( + _wait_for_combo_box_value( + gui_submitter_locators.deadline_cloud_settings_farm_dropdown, farm_name + ), + f"Farm combo box should show '{farm_name}' after async refresh", + ) + + +def set_queue_name(queue_name: str): + """Set the queue in the Submit Dialog's Deadline Cloud settings.""" + # Ensure we're on the Shared job settings tab + navigate_shared_job_settings() + # Open queue dropdown menu + squish.mouseClick( + squish.waitForObject(gui_submitter_locators.deadline_cloud_settings_queue_dropdown) + ) + test.log("Opened queue name drop down menu in Submit Dialog.") + test.compare( + squish.waitForObjectExists( + gui_submitter_locators.queue_name_dropdown_locator(queue_name) + ).text, + queue_name, + "Expect queue name to be present in drop down.", + ) + # Select the queue + squish.mouseClick( + squish.waitForObjectItem( + gui_submitter_locators.deadline_cloud_settings_queue_dropdown, queue_name + ) + ) + test.log(f"Selected queue name: {queue_name}") + # Wait for async refresh to complete + test.verify( + _wait_for_combo_box_value( + gui_submitter_locators.deadline_cloud_settings_queue_dropdown, queue_name + ), + f"Queue combo box should show '{queue_name}' after async refresh", + ) + + +def set_storage_profile(storage_profile: str): + """Set the storage profile in the Submit Dialog's Deadline Cloud settings.""" + # Ensure we're on the Shared job settings tab + navigate_shared_job_settings() + # Open storage profile dropdown menu + squish.mouseClick( + squish.waitForObject( + gui_submitter_locators.deadline_cloud_settings_storage_profile_dropdown + ) + ) + test.log("Opened storage profile drop down menu in Submit Dialog.") + test.compare( + squish.waitForObjectExists( + gui_submitter_locators.storage_profile_dropdown_locator(storage_profile) + ).text, + storage_profile, + "Expect storage profile to be present in drop down.", + ) + # Select the storage profile + squish.mouseClick( + squish.waitForObjectItem( + gui_submitter_locators.deadline_cloud_settings_storage_profile_dropdown, + storage_profile, + ) + ) + test.log(f"Selected storage profile: {storage_profile}") + # Wait for async refresh to complete + test.verify( + _wait_for_combo_box_value( + gui_submitter_locators.deadline_cloud_settings_storage_profile_dropdown, storage_profile + ), + f"Storage profile combo box should show '{storage_profile}' after async refresh", + ) + + +def set_and_verify_os_storage_profile( + linux_storage_profile: str, windows_storage_profile: str, macos_storage_profile: str +): + """Set and verify storage profile based on OS platform being tested.""" + if platform.system() == "Linux": + test.log("Detected test running on Linux OS") + set_storage_profile(linux_storage_profile) + test.compare( + str( + squish.waitForObjectExists( + gui_submitter_locators.deadline_cloud_settings_storage_profile_dropdown + ).currentText + ), + linux_storage_profile, + "Expect selected storage profile to be set.", + ) + elif platform.system() == "Windows": + test.log("Detected test running on Windows OS") + set_storage_profile(windows_storage_profile) + test.compare( + str( + squish.waitForObjectExists( + gui_submitter_locators.deadline_cloud_settings_storage_profile_dropdown + ).currentText + ), + windows_storage_profile, + "Expect selected storage profile to be set.", + ) + elif platform.system() == "Darwin": + test.log("Detected test running on macOS") + set_storage_profile(macos_storage_profile) + test.compare( + str( + squish.waitForObjectExists( + gui_submitter_locators.deadline_cloud_settings_storage_profile_dropdown + ).currentText + ), + macos_storage_profile, + "Expect selected storage profile to be set.", + ) + test.log("Selected and verified storage profile based on OS platform being tested.") diff --git a/test/squish/suite_deadline_gui/shared/scripts/gui_submitter_locators.py b/test/squish/suite_deadline_gui/shared/scripts/gui_submitter_locators.py index 68d48b91c..d180daba9 100644 --- a/test/squish/suite_deadline_gui/shared/scripts/gui_submitter_locators.py +++ b/test/squish/suite_deadline_gui/shared/scripts/gui_submitter_locators.py @@ -5,7 +5,7 @@ "type": "SubmitJobToDeadlineDialog", "unnamed": 1, "visible": 1, - "windowTitle": "Submit to AWS Deadline Cloud", + "windowTitle": "Deadline Cloud JobBundle Submitter", } # Settings button settings_button = { @@ -90,19 +90,26 @@ "unnamed": 1, "visible": 1, } -# Deadline Cloud Squish Farm text element -deadline_cloud_settings_farm_name = { +# Farm combo box +deadline_cloud_settings_farm_dropdown = { "container": deadline_cloud_settings_widget, - "text": "Deadline Cloud Squish Farm", - "type": "QLabel", + "type": "QComboBox", "unnamed": 1, "visible": 1, } -# Squish Automation Queue text element -deadline_cloud_settings_queue_name = { +# Queue combo box +deadline_cloud_settings_queue_dropdown = { "container": deadline_cloud_settings_widget, - "text": "Squish Automation Queue", - "type": "QLabel", + "occurrence": 2, + "type": "QComboBox", + "unnamed": 1, + "visible": 1, +} +# Storage profile combo box +deadline_cloud_settings_storage_profile_dropdown = { + "container": deadline_cloud_settings_widget, + "occurrence": 3, + "type": "QComboBox", "unnamed": 1, "visible": 1, } @@ -222,13 +229,13 @@ "unnamed": 1, "visible": 1, } -# load different job bundle button in AWS Submitter dialogue (job-specific settings tab) +# load different job bundle button in AWS Submitter dialogue (in button box at bottom) load_different_job_bundle_button = { - "container": properties_only_widget, - "text": "Load a different job bundle", + "text": "Load Bundle", "type": "QPushButton", "unnamed": 1, "visible": 1, + "window": aws_submitter_dialogue, } # job history directory default filepath input job_hist_dir_dropdown = { @@ -239,21 +246,25 @@ } -def deadlinecloud_farmname_locator(farm_name): +def farm_name_dropdown_locator(farm_name): return { - "container": deadline_cloud_settings_widget, + "container": deadline_cloud_settings_farm_dropdown, "text": farm_name, - "type": "QLabel", - "unnamed": 1, - "visible": 1, + "type": "QModelIndex", } -def deadlinecloud_queuename_locator(queue_name): +def queue_name_dropdown_locator(queue_name): return { - "container": deadline_cloud_settings_widget, + "container": deadline_cloud_settings_queue_dropdown, "text": queue_name, - "type": "QLabel", - "unnamed": 1, - "visible": 1, + "type": "QModelIndex", + } + + +def storage_profile_dropdown_locator(storage_profile): + return { + "container": deadline_cloud_settings_storage_profile_dropdown, + "text": storage_profile, + "type": "QModelIndex", } diff --git a/test/squish/suite_deadline_gui/shared/scripts/names.py b/test/squish/suite_deadline_gui/shared/scripts/names.py index 2a65a122e..0b38191cc 100644 --- a/test/squish/suite_deadline_gui/shared/scripts/names.py +++ b/test/squish/suite_deadline_gui/shared/scripts/names.py @@ -142,23 +142,12 @@ "visible": 1, "window": aWS_Deadline_Cloud_workstation_configuration_DeadlineConfigDialog, } -profile_settings_QComboBox = { - "container": aWS_Deadline_Cloud_workstation_configuration_Profile_settings_QGroupBox, - "type": "QComboBox", - "unnamed": 1, - "visible": 1, -} profile_settings_QLineEdit = { "container": aWS_Deadline_Cloud_workstation_configuration_Profile_settings_QGroupBox, "type": "QLineEdit", "unnamed": 1, "visible": 1, } -deadline_Cloud_Squish_Farm_QModelIndex = { - "container": profile_settings_QComboBox, - "text": "Deadline Cloud Squish Farm", - "type": "QModelIndex", -} profile_settings_QPushButton = { "container": aWS_Deadline_Cloud_workstation_configuration_Profile_settings_QGroupBox, "text": "...", @@ -181,36 +170,6 @@ "visible": 1, "window": aWS_Deadline_Cloud_workstation_configuration_DeadlineConfigDialog, } -farm_settings_QComboBox = { - "container": aWS_Deadline_Cloud_workstation_configuration_Farm_settings_QGroupBox, - "type": "QComboBox", - "unnamed": 1, - "visible": 1, -} -farm_settings_QComboBox_2 = { - "container": aWS_Deadline_Cloud_workstation_configuration_Farm_settings_QGroupBox, - "occurrence": 2, - "type": "QComboBox", - "unnamed": 1, - "visible": 1, -} -farm_settings_QComboBox_3 = { - "container": aWS_Deadline_Cloud_workstation_configuration_Farm_settings_QGroupBox, - "occurrence": 3, - "type": "QComboBox", - "unnamed": 1, - "visible": 1, -} -squish_Automation_Queue_QModelIndex = { - "container": farm_settings_QComboBox, - "text": "Squish Automation Queue", - "type": "QModelIndex", -} -squish_Storage_Profile_QModelIndex = { - "container": farm_settings_QComboBox_2, - "text": "Squish Storage Profile", - "type": "QModelIndex", -} farm_settings_Job_attachments_filesystem_options_QLabel = { "container": aWS_Deadline_Cloud_workstation_configuration_Farm_settings_QGroupBox, "text": "Job attachments filesystem options", @@ -331,7 +290,7 @@ "type": "SubmitJobToDeadlineDialog", "unnamed": 1, "visible": 1, - "windowTitle": "Submit to AWS Deadline Cloud", + "windowTitle": "Deadline Cloud JobBundle Submitter", } submit_to_AWS_Deadline_Cloud_Settings_QPushButton = { "text": "Settings...", @@ -418,20 +377,6 @@ "unnamed": 1, "visible": 1, } -deadline_Cloud_settings_Deadline_Cloud_Squish_Farm_QLabel = { - "container": deadline_Cloud_settings_DeadlineCloudSettingsWidget, - "text": "Deadline Cloud Squish Farm", - "type": "QLabel", - "unnamed": 1, - "visible": 1, -} -deadline_Cloud_settings_Squish_Automation_Queue_QLabel = { - "container": deadline_Cloud_settings_DeadlineCloudSettingsWidget, - "text": "Squish Automation Queue", - "type": "QLabel", - "unnamed": 1, - "visible": 1, -} queue_Environment_Conda_JobTemplateGroupLayout = { "container": qt_tabwidget_stackedwidget_QScrollArea, "name": "Queue Environment: Conda", @@ -566,11 +511,11 @@ "visible": 1, } load_a_different_job_bundle_QPushButton = { - "container": qt_tabwidget_stackedwidget_QScrollArea, - "text": "Load a different job bundle", + "text": "Load Bundle", "type": "QPushButton", "unnamed": 1, "visible": 1, + "window": submit_to_AWS_Deadline_Cloud_SubmitJobToDeadlineDialog, } lookInCombo_QComboBox = { "container": qt_tabwidget_stackedwidget_QScrollArea, diff --git a/test/squish/suite_deadline_gui/shared/scripts/workstation_config_helpers.py b/test/squish/suite_deadline_gui/shared/scripts/workstation_config_helpers.py index 324d039e0..7fbece268 100644 --- a/test/squish/suite_deadline_gui/shared/scripts/workstation_config_helpers.py +++ b/test/squish/suite_deadline_gui/shared/scripts/workstation_config_helpers.py @@ -87,138 +87,6 @@ def hit_apply_button(): test.log("Settings have been applied.") -def set_farm_name(farm_name: str): - # open Default farm drop down menu - squish.mouseClick( - squish.waitForObject(workstation_config_locators.profilesettings_defaultfarm_dropdown) - ) - test.log("Opened farm name drop down menu.") - test.compare( - squish.waitForObjectExists(workstation_config_locators.farm_name_locator(farm_name)).text, - farm_name, - "Expect farm name to be present in drop down.", - ) - # select Default farm - squish.mouseClick( - squish.waitForObjectItem( - workstation_config_locators.profilesettings_defaultfarm_dropdown, farm_name - ) - ) - test.log("Selected farm name.") - - -def set_queue_name(queue_name: str): - # open Default queue drop down menu - squish.mouseClick( - squish.waitForObject(workstation_config_locators.farmsettings_defaultqueue_dropdown) - ) - test.log("Opened queue name drop down menu.") - test.compare( - squish.waitForObjectExists(workstation_config_locators.queue_name_locator(queue_name)).text, - queue_name, - "Expect queue name to be present in drop down.", - ) - # select Default queue - squish.mouseClick( - squish.waitForObjectItem( - workstation_config_locators.farmsettings_defaultqueue_dropdown, queue_name - ) - ) - test.log("Selected queue name.") - - -def set_and_verify_os_storage_profile( - linux_storage_profile: str, windows_storage_profile: str, macos_storage_profile: str -): - # open Default storage profile drop down menu - squish.mouseClick( - squish.waitForObject( - workstation_config_locators.farmsettings_defaultstorageprofile_dropdown - ) - ) - test.log("Opened storage profile drop down menu.") - # select storage profile based on OS platform being tested - if platform.system() == "Linux": - test.log("Detected test running on Linux OS") - test.compare( - squish.waitForObjectExists( - workstation_config_locators.storage_profile_locator(linux_storage_profile) - ).text, - linux_storage_profile, - "Expect Linux Storage Profile to be present in drop down.", - ) - # select Linux Storage Profile - squish.mouseClick( - squish.waitForObjectItem( - workstation_config_locators.farmsettings_defaultstorageprofile_dropdown, - linux_storage_profile, - ) - ) - # verify correct storage profile name is set - test.compare( - str( - squish.waitForObjectExists( - workstation_config_locators.farmsettings_defaultstorageprofile_dropdown - ).currentText - ), - linux_storage_profile, - "Expect selected storage profile to be set.", - ) - elif platform.system() == "Windows": - test.log("Detected test running on Windows OS") - test.compare( - squish.waitForObjectExists( - workstation_config_locators.storage_profile_locator(windows_storage_profile) - ).text, - windows_storage_profile, - "Expect Windows Storage Profile to be present in drop down.", - ) - # select Windows Storage Profile - squish.mouseClick( - squish.waitForObjectItem( - workstation_config_locators.farmsettings_defaultstorageprofile_dropdown, - windows_storage_profile, - ) - ) - # verify correct storage profile name is set - test.compare( - str( - squish.waitForObjectExists( - workstation_config_locators.farmsettings_defaultstorageprofile_dropdown - ).currentText - ), - windows_storage_profile, - "Expect selected storage profile to be set.", - ) - elif platform.system() == "Darwin": - test.log("Detected test running on macOS") - test.compare( - squish.waitForObjectExists( - workstation_config_locators.storage_profile_locator(macos_storage_profile) - ).text, - macos_storage_profile, - "Expect macOS Storage Profile to be present in drop down.", - ) - # select macOS Storage Profile - squish.mouseClick( - squish.waitForObjectItem( - workstation_config_locators.farmsettings_defaultstorageprofile_dropdown, - macos_storage_profile, - ) - ) - # verify correct storage profile name is set - test.compare( - str( - squish.waitForObjectExists( - workstation_config_locators.farmsettings_defaultstorageprofile_dropdown - ).currentText - ), - macos_storage_profile, - "Expect selected storage profile to be set.", - ) - test.log("Selected storage profile based on OS platform being tested.") - - def set_job_attachments_filesystem_options(job_attachments: str): # open Job attachments filesystem options drop down menu squish.mouseClick( diff --git a/test/squish/suite_deadline_gui/shared/scripts/workstation_config_locators.py b/test/squish/suite_deadline_gui/shared/scripts/workstation_config_locators.py index fedea6846..c5d472ce5 100644 --- a/test/squish/suite_deadline_gui/shared/scripts/workstation_config_locators.py +++ b/test/squish/suite_deadline_gui/shared/scripts/workstation_config_locators.py @@ -61,9 +61,10 @@ "visible": 1, "window": deadline_config_dialog, } -profilesettings_defaultfarm_dropdown = { +profilesettings_jobhistdir_label = { "container": deadlinedialog_profilesettings_box, - "type": "QComboBox", + "text": "Job history directory", + "type": "QLabel", "unnamed": 1, "visible": 1, } @@ -73,12 +74,6 @@ "unnamed": 1, "visible": 1, } -# Deadline Cloud Squish Farm element -deadlinecloudsquish_defaultfarm_index = { - "container": profilesettings_defaultfarm_dropdown, - "text": "Deadline Cloud Squish Farm", - "type": "QModelIndex", -} # choose job history directory file browser open_job_hist_dir_button = { @@ -105,38 +100,12 @@ "visible": 1, "window": deadline_config_dialog, } -farmsettings_defaultqueue_dropdown = { - "container": deadlinedialog_farmsettings_box, - "type": "QComboBox", - "unnamed": 1, - "visible": 1, -} -farmsettings_defaultstorageprofile_dropdown = { - "container": deadlinedialog_farmsettings_box, - "occurrence": 2, - "type": "QComboBox", - "unnamed": 1, - "visible": 1, -} farmsettings_jobattachmentsoptions_dropdown = { "container": deadlinedialog_farmsettings_box, - "occurrence": 3, "type": "QComboBox", "unnamed": 1, "visible": 1, } -# Squish Automation Queue element -squishautomationqueue_defaultqueue_index = { - "container": farmsettings_defaultqueue_dropdown, - "text": "Squish Automation Queue", - "type": "QModelIndex", -} -# Squish Storage Profile element -squishstorageprofile_defaultstorageprofile_index = { - "container": farmsettings_defaultstorageprofile_dropdown, - "text": "Squish Storage Profile", - "type": "QModelIndex", -} jobattachments_filesystemoptions_text_label = { "container": deadlinedialog_farmsettings_box, "text": "Job attachments filesystem options", @@ -225,27 +194,3 @@ def profile_name_locator(profile_name): "text": profile_name, "type": "QModelIndex", } - - -def farm_name_locator(farm_name): - return { - "container": profilesettings_defaultfarm_dropdown, - "text": farm_name, - "type": "QModelIndex", - } - - -def queue_name_locator(queue_name): - return { - "container": farmsettings_defaultqueue_dropdown, - "text": queue_name, - "type": "QModelIndex", - } - - -def storage_profile_locator(storage_profile): - return { - "container": farmsettings_defaultstorageprofile_dropdown, - "text": storage_profile, - "type": "QModelIndex", - } diff --git a/test/squish/suite_deadline_gui/suite.conf b/test/squish/suite_deadline_gui/suite.conf index 3e981abf9..a7f774235 100644 --- a/test/squish/suite_deadline_gui/suite.conf +++ b/test/squish/suite_deadline_gui/suite.conf @@ -1,8 +1,9 @@ +AUT=deadline ENVVARS=envvars HOOK_SUB_PROCESSES=true IMPLICITAUTSTART=0 LANGUAGE=Python OBJECTMAPSTYLE=script -TEST_CASES=tst_verify_settings_dialogue tst_verify_gui_submitter_bundles +TEST_CASES=tst_verify_settings_dialogue tst_verify_gui_submitter_bundles tst_verify_submitter_deadline_cloud_settings VERSION=3 WRAPPERS=Qt diff --git a/test/squish/suite_deadline_gui/tst_verify_gui_submitter_bundles/test.py b/test/squish/suite_deadline_gui/tst_verify_gui_submitter_bundles/test.py index 7e059d259..f953aaa7d 100644 --- a/test/squish/suite_deadline_gui/tst_verify_gui_submitter_bundles/test.py +++ b/test/squish/suite_deadline_gui/tst_verify_gui_submitter_bundles/test.py @@ -37,7 +37,7 @@ def main(): # verify GUI Submitter dialogue opens test.compare( str(squish.waitForObjectExists(gui_submitter_locators.aws_submitter_dialogue).windowTitle), - "Submit to AWS Deadline Cloud", + "Deadline Cloud JobBundle Submitter", "Expect AWS Deadline Cloud Submitter window title to be present.", ) test.compare( diff --git a/test/squish/suite_deadline_gui/tst_verify_settings_dialogue/test.py b/test/squish/suite_deadline_gui/tst_verify_settings_dialogue/test.py index 96f03d776..1eef6091b 100644 --- a/test/squish/suite_deadline_gui/tst_verify_settings_dialogue/test.py +++ b/test/squish/suite_deadline_gui/tst_verify_settings_dialogue/test.py @@ -41,34 +41,6 @@ def main(): jobhistory_dir_helpers.verify_job_hist_dir_text_input(config.custom_job_hist_dir) # verify custom job history folder is created/exists in user's system jobhistory_dir_helpers.verify_directory_exists(config.custom_job_hist_dir) - # set farm name - workstation_config_helpers.set_farm_name(config.farm_name) - # verify correct farm name is set - test.compare( - str( - squish.waitForObjectExists( - workstation_config_locators.profilesettings_defaultfarm_dropdown - ).currentText - ), - config.farm_name, - "Expect selected farm name to be set.", - ) - # set queue name - workstation_config_helpers.set_queue_name(config.queue_name) - # verify correct queue name is set - test.compare( - str( - squish.waitForObjectExists( - workstation_config_locators.farmsettings_defaultqueue_dropdown - ).currentText - ), - config.queue_name, - "Expect selected queue name to be set.", - ) - # set and verify storage profile based on OS platform being tested - workstation_config_helpers.set_and_verify_os_storage_profile( - config.storage_profile_linux, config.storage_profile_windows, config.storage_profile_macos - ) # set job attachments filesystem options workstation_config_helpers.set_job_attachments_filesystem_options(config.job_attachments) # verify job attachments filesystem options is set to 'COPIED' diff --git a/test/squish/suite_deadline_gui/tst_verify_submitter_deadline_cloud_settings/test.py b/test/squish/suite_deadline_gui/tst_verify_submitter_deadline_cloud_settings/test.py new file mode 100644 index 000000000..b518c1da8 --- /dev/null +++ b/test/squish/suite_deadline_gui/tst_verify_submitter_deadline_cloud_settings/test.py @@ -0,0 +1,131 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# -*- coding: utf-8 -*- +# mypy: disable-error-code="attr-defined" + +import config +import choose_jobbundledir_helpers +import choose_jobbundledir_locators +import gui_submitter_helpers +import gui_submitter_locators +import squish +import test + + +def init(): + # launch Choose Job Bundle GUI Submitter based on OS platform being tested + choose_jobbundledir_helpers.detect_platform_and_launch_jobbundle_guisubmitter() + # verify Choose job bundle directory is open + test.compare( + str( + squish.waitForObjectExists( + choose_jobbundledir_locators.choose_job_bundle_dir + ).windowTitle + ), + "Choose job bundle directory", + "Expect Choose job bundle directory window title to be present.", + ) + test.compare( + squish.waitForObjectExists(choose_jobbundledir_locators.choose_job_bundle_dir).visible, + True, + "Expect Choose job bundle directory to be open.", + ) + # select a job bundle to open the Submit Dialog + choose_jobbundledir_helpers.select_jobbundle(config.simple_ui_with_ja) + # verify GUI Submitter dialogue opens + test.compare( + str(squish.waitForObjectExists(gui_submitter_locators.aws_submitter_dialogue).windowTitle), + "Deadline Cloud JobBundle Submitter", + "Expect AWS Deadline Cloud Submitter window title to be present.", + ) + test.compare( + squish.waitForObjectExists(gui_submitter_locators.aws_submitter_dialogue).visible, + True, + "Expect AWS Deadline Cloud Submitter to be open.", + ) + + +def main(): + # Navigate to Shared job settings tab where Deadline Cloud settings live + gui_submitter_helpers.navigate_shared_job_settings() + + # --- Test farm selection and verify queue list refreshes --- + test.log("Testing farm selection in Submit Dialog") + gui_submitter_helpers.set_farm_name(config.farm_name) + # verify correct farm name is set + test.compare( + str( + squish.waitForObjectExists( + gui_submitter_locators.deadline_cloud_settings_farm_dropdown + ).currentText + ), + config.farm_name, + "Expect selected farm name to be set.", + ) + + # --- Test queue selection (verifies queue list refreshed after farm change) --- + test.log( + "Testing queue selection in Submit Dialog (verifies queue list refreshed after farm change)" + ) + gui_submitter_helpers.set_queue_name(config.queue_name) + # verify correct queue name is set + test.compare( + str( + squish.waitForObjectExists( + gui_submitter_locators.deadline_cloud_settings_queue_dropdown + ).currentText + ), + config.queue_name, + "Expect selected queue name to be set.", + ) + + # --- Test storage profile selection (verifies storage profile list refreshed after queue change) --- + test.log( + "Testing storage profile selection in Submit Dialog (verifies list refreshed after queue change)" + ) + gui_submitter_helpers.set_and_verify_os_storage_profile( + config.storage_profile_linux, + config.storage_profile_windows, + config.storage_profile_macos, + ) + + # --- Verify cascade: change farm again and confirm queue/storage profile update --- + test.log("Testing cascade: re-selecting farm to verify queue and storage profile lists refresh") + # Re-select the same farm to trigger a refresh of dependent lists + gui_submitter_helpers.set_farm_name(config.farm_name) + test.compare( + str( + squish.waitForObjectExists( + gui_submitter_locators.deadline_cloud_settings_farm_dropdown + ).currentText + ), + config.farm_name, + "Expect farm name still set after re-selection.", + ) + # After farm re-selection, queue list should have refreshed - verify we can still select the queue + gui_submitter_helpers.set_queue_name(config.queue_name) + test.compare( + str( + squish.waitForObjectExists( + gui_submitter_locators.deadline_cloud_settings_queue_dropdown + ).currentText + ), + config.queue_name, + "Expect queue name set after farm re-selection (queue list refreshed).", + ) + # After queue re-selection, storage profile list should have refreshed + gui_submitter_helpers.set_and_verify_os_storage_profile( + config.storage_profile_linux, + config.storage_profile_windows, + config.storage_profile_macos, + ) + + test.log( + "All Deadline Cloud settings (farm, queue, storage profile) and refresh cascade verified in Submit Dialog." + ) + + +def cleanup(): + test.log("Closing AWS Submitter dialogue by sending QCloseEvent to 'x' button.") + squish.sendEvent( + "QCloseEvent", squish.waitForObject(gui_submitter_locators.aws_submitter_dialogue) + ) diff --git a/test/unit/deadline_client/ui/dialogs/test_help_dialog.py b/test/unit/deadline_client/ui/dialogs/test_help_dialog.py index 4e0c12012..c24f281de 100644 --- a/test/unit/deadline_client/ui/dialogs/test_help_dialog.py +++ b/test/unit/deadline_client/ui/dialogs/test_help_dialog.py @@ -221,9 +221,12 @@ def test_hard_coded_documentation_links_display(self, qtbot): # Get the layout and find all QLabel widgets layout = help_dialog.layout() + assert layout is not None labels = [] for i in range(layout.count()): - widget = layout.itemAt(i).widget() + layout_item = layout.itemAt(i) + assert layout_item is not None + widget = layout_item.widget() if isinstance(widget, QLabel): labels.append(widget) diff --git a/test/unit/deadline_client/ui/dialogs/test_submit_job_to_deadline_dialog.py b/test/unit/deadline_client/ui/dialogs/test_submit_job_to_deadline_dialog.py index 3819e554c..2fe26b1fc 100644 --- a/test/unit/deadline_client/ui/dialogs/test_submit_job_to_deadline_dialog.py +++ b/test/unit/deadline_client/ui/dialogs/test_submit_job_to_deadline_dialog.py @@ -14,12 +14,16 @@ @pytest.fixture def mock_job_settings_widget(): - """Create a mock job settings widget type.""" - widget = MagicMock() - widget.return_value = MagicMock() - widget.return_value.parameter_changed = MagicMock() - widget.return_value.parameter_changed.connect = MagicMock() - return widget + """Create a mock job settings widget type that returns a real QWidget.""" + from qtpy.QtWidgets import QWidget + + class MockJobSettingsWidget(QWidget): + def __init__(self, initial_settings=None, parent=None): + super().__init__(parent) + self.parameter_changed = MagicMock() + self.parameter_changed.connect = MagicMock() + + return MockJobSettingsWidget @patch("deadline.client.ui.dialogs.submit_job_to_deadline_dialog.DeadlineAuthenticationStatus") diff --git a/test/unit/deadline_client/ui/widgets/test_host_requirements_tab.py b/test/unit/deadline_client/ui/widgets/test_host_requirements_tab.py index 6001fcf09..4c216f997 100644 --- a/test/unit/deadline_client/ui/widgets/test_host_requirements_tab.py +++ b/test/unit/deadline_client/ui/widgets/test_host_requirements_tab.py @@ -59,7 +59,9 @@ def test_input_in_hardware_requirements_widget_should_be_integer_within_range(qt def test_name_in_custom_amount_widget_should_be_truncated(qtbot): - widget = CustomAmountWidget(MagicMock(), 1) + parent = CustomRequirementsWidget() + qtbot.addWidget(parent) + widget = CustomAmountWidget(MagicMock(), 1, parent) qtbot.addWidget(widget) invalid_str = "a" * (AMOUNT_NAME_MAX_LENGTH + 1) @@ -68,7 +70,9 @@ def test_name_in_custom_amount_widget_should_be_truncated(qtbot): def test_name_in_custom_amount_widget_should_not_allow_invalid_chars(qtbot): - widget = CustomAmountWidget(MagicMock(), 1) + parent = CustomRequirementsWidget() + qtbot.addWidget(parent) + widget = CustomAmountWidget(MagicMock(), 1, parent) qtbot.addWidget(widget) invalid_str = "" @@ -77,7 +81,9 @@ def test_name_in_custom_amount_widget_should_not_allow_invalid_chars(qtbot): def test_name_in_custom_amount_widget_should_allow_identifiers(qtbot): - widget = CustomAmountWidget(MagicMock(), 1) + parent = CustomRequirementsWidget() + qtbot.addWidget(parent) + widget = CustomAmountWidget(MagicMock(), 1, parent) qtbot.addWidget(widget) valid_identifier = "a" + (".a" * math.floor((AMOUNT_NAME_MAX_LENGTH - 1) / 2)) @@ -86,7 +92,9 @@ def test_name_in_custom_amount_widget_should_allow_identifiers(qtbot): def test_name_in_custom_amount_widget_does_not_allow_invalid_identifiers(qtbot): - widget = CustomAmountWidget(MagicMock(), 1) + parent = CustomRequirementsWidget() + qtbot.addWidget(parent) + widget = CustomAmountWidget(MagicMock(), 1, parent) qtbot.addWidget(widget) valid_identifier = "a" @@ -97,7 +105,9 @@ def test_name_in_custom_amount_widget_does_not_allow_invalid_identifiers(qtbot): def test_name_in_custom_amount_widget_should_not_allow_missing_identifiers(qtbot): - widget = CustomAmountWidget(MagicMock(), 1) + parent = CustomRequirementsWidget() + qtbot.addWidget(parent) + widget = CustomAmountWidget(MagicMock(), 1, parent) qtbot.addWidget(widget) missing_identifier = "a..a" @@ -106,7 +116,9 @@ def test_name_in_custom_amount_widget_should_not_allow_missing_identifiers(qtbot def test_name_in_custom_amount_widget_should_not_allow_reserved_first_identifier(qtbot): - widget = CustomAmountWidget(MagicMock(), 1) + parent = CustomRequirementsWidget() + qtbot.addWidget(parent) + widget = CustomAmountWidget(MagicMock(), 1, parent) qtbot.addWidget(widget) for reserved_identifier in RESERVED_FIRST_IDENTIFIERS: @@ -122,7 +134,9 @@ def test_name_in_custom_amount_widget_should_not_allow_reserved_first_identifier def test_value_in_custom_amount_widget_should_be_integer_within_range(qtbot): - widget = CustomAmountWidget(MagicMock(), 1) + parent = CustomRequirementsWidget() + qtbot.addWidget(parent) + widget = CustomAmountWidget(MagicMock(), 1, parent) qtbot.addWidget(widget) assert widget.min_spin_box.min == 0 @@ -235,7 +249,9 @@ def test_name_in_custom_attribute_widget_should_not_allow_reserved_first_identif def test_custom_amount_widget_includes_zero_minimum(qtbot): - widget = CustomAmountWidget(MagicMock(), 1) + parent = CustomRequirementsWidget() + qtbot.addWidget(parent) + widget = CustomAmountWidget(MagicMock(), 1, parent) qtbot.addWidget(widget) widget.name_line_edit.setText("test.amount") @@ -250,7 +266,9 @@ def test_custom_amount_widget_includes_zero_minimum(qtbot): def test_custom_amount_widget_includes_zero_maximum(qtbot): - widget = CustomAmountWidget(MagicMock(), 1) + parent = CustomRequirementsWidget() + qtbot.addWidget(parent) + widget = CustomAmountWidget(MagicMock(), 1, parent) qtbot.addWidget(widget) widget.name_line_edit.setText("test.amount") @@ -265,7 +283,9 @@ def test_custom_amount_widget_includes_zero_maximum(qtbot): def test_custom_amount_widget_includes_zero_only_minimum(qtbot): - widget = CustomAmountWidget(MagicMock(), 1) + parent = CustomRequirementsWidget() + qtbot.addWidget(parent) + widget = CustomAmountWidget(MagicMock(), 1, parent) qtbot.addWidget(widget) widget.name_line_edit.setText("test.amount") @@ -279,7 +299,9 @@ def test_custom_amount_widget_includes_zero_only_minimum(qtbot): def test_custom_amount_widget_includes_zero_only_maximum(qtbot): - widget = CustomAmountWidget(MagicMock(), 1) + parent = CustomRequirementsWidget() + qtbot.addWidget(parent) + widget = CustomAmountWidget(MagicMock(), 1, parent) qtbot.addWidget(widget) widget.name_line_edit.setText("test.amount") diff --git a/test/unit/deadline_client/ui/widgets/test_shared_job_settings_tab.py b/test/unit/deadline_client/ui/widgets/test_shared_job_settings_tab.py index e9ad7f75b..d3fa892b8 100644 --- a/test/unit/deadline_client/ui/widgets/test_shared_job_settings_tab.py +++ b/test/unit/deadline_client/ui/widgets/test_shared_job_settings_tab.py @@ -1,6 +1,7 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. import pytest +from typing import Generator from unittest.mock import patch, MagicMock try: @@ -89,16 +90,18 @@ def test_max_worker_count_should_be_integer_within_range( # Tests for DeadlineCloudSettingsWidget farm/queue combo boxes +MOCK_CONFIG_PATH = "deadline.client.ui.widgets.shared_job_settings_tab.config_file.read_config" +MOCK_SET_SETTING_PATH = "deadline.client.ui.widgets.shared_job_settings_tab.set_setting" + + @pytest.fixture(scope="function") -def deadline_cloud_settings_widget(qtbot) -> DeadlineCloudSettingsWidget: - """Fixture for DeadlineCloudSettingsWidget with mocked combo boxes.""" - with patch( - "deadline.client.ui.widgets.shared_job_settings_tab.config_file.read_config" - ) as mock_config: +def deadline_cloud_settings_widget(qtbot) -> Generator[DeadlineCloudSettingsWidget, None, None]: + """Fixture for DeadlineCloudSettingsWidget with mocked config.""" + with patch(MOCK_CONFIG_PATH) as mock_config: mock_config.return_value = MagicMock() widget = DeadlineCloudSettingsWidget() qtbot.addWidget(widget) - return widget + yield widget def test_farm_selection_updates_config(deadline_cloud_settings_widget: DeadlineCloudSettingsWidget): @@ -106,16 +109,12 @@ def test_farm_selection_updates_config(deadline_cloud_settings_widget: DeadlineC widget = deadline_cloud_settings_widget test_farm_id = "farm-test123" - # Add a test item to the farm combo box + # Add two items so selecting the second one triggers currentIndexChanged + widget.farm_box.box.addItem("Default Farm", "farm-default") widget.farm_box.box.addItem("Test Farm", test_farm_id) - with patch( - "deadline.client.ui.widgets.shared_job_settings_tab.set_setting" - ) as mock_set_setting: - # Select the farm (triggers _on_farm_changed) - widget.farm_box.box.setCurrentIndex(widget.farm_box.box.count() - 1) - - # Verify config was updated with the farm ID + with patch(MOCK_SET_SETTING_PATH) as mock_set_setting: + widget.farm_box.box.setCurrentIndex(1) mock_set_setting.assert_called_with("defaults.farm_id", test_farm_id) @@ -126,16 +125,11 @@ def test_queue_selection_updates_config( widget = deadline_cloud_settings_widget test_queue_id = "queue-test456" - # Add a test item to the queue combo box + widget.queue_box.box.addItem("Default Queue", "queue-default") widget.queue_box.box.addItem("Test Queue", test_queue_id) - with patch( - "deadline.client.ui.widgets.shared_job_settings_tab.set_setting" - ) as mock_set_setting: - # Select the queue (triggers _on_queue_changed) - widget.queue_box.box.setCurrentIndex(widget.queue_box.box.count() - 1) - - # Verify config was updated with the queue ID + with patch(MOCK_SET_SETTING_PATH) as mock_set_setting: + widget.queue_box.box.setCurrentIndex(1) mock_set_setting.assert_called_with("defaults.queue_id", test_queue_id) @@ -144,18 +138,15 @@ def test_farm_change_refreshes_queue_list( ): """Test that changing farm triggers queue list refresh.""" widget = deadline_cloud_settings_widget - test_farm_id = "farm-test789" - - # Add a test item to the farm combo box - widget.farm_box.box.addItem("Test Farm", test_farm_id) - with patch.object(widget.queue_box, "refresh_list") as mock_refresh: - with patch("deadline.client.ui.widgets.shared_job_settings_tab.set_setting"): - # Select the farm - widget.farm_box.box.setCurrentIndex(widget.farm_box.box.count() - 1) + widget.farm_box.box.addItem("Default Farm", "farm-default") + widget.farm_box.box.addItem("Test Farm", "farm-test789") - # Verify queue list was refreshed - mock_refresh.assert_called_once() + with patch.object(widget.queue_box, "refresh_list") as mock_refresh, patch( + MOCK_SET_SETTING_PATH + ): + widget.farm_box.box.setCurrentIndex(1) + mock_refresh.assert_called_once() def test_refresh_setting_controls_updates_combo_boxes( @@ -172,13 +163,9 @@ def test_refresh_setting_controls_updates_combo_boxes( widget.queue_box, "refresh_selected_id" ) as mock_queue_refresh, patch.object( widget.farm_box, "refresh_list" - ) as mock_farm_list, patch.object(widget.queue_box, "refresh_list") as mock_queue_list, patch( - "deadline.client.ui.widgets.shared_job_settings_tab.config_file.read_config" - ): - # Call refresh with authorized=True + ) as mock_farm_list, patch.object(widget.queue_box, "refresh_list") as mock_queue_list: widget.refresh_setting_controls(deadline_authorized=True) - # Verify all methods were called mock_farm_config.assert_called_once() mock_queue_config.assert_called_once() mock_farm_refresh.assert_called_once() @@ -199,13 +186,9 @@ def test_refresh_setting_controls_skips_list_refresh_when_unauthorized( widget.queue_box, "refresh_selected_id" ), patch.object(widget.farm_box, "refresh_list") as mock_farm_list, patch.object( widget.queue_box, "refresh_list" - ) as mock_queue_list, patch( - "deadline.client.ui.widgets.shared_job_settings_tab.config_file.read_config" - ): - # Call refresh with authorized=False + ) as mock_queue_list: widget.refresh_setting_controls(deadline_authorized=False) - # Verify list refresh was NOT called mock_farm_list.assert_not_called() mock_queue_list.assert_not_called() @@ -216,9 +199,8 @@ def test_storage_profile_hidden_by_default( """Test that storage profile selector is hidden by default.""" widget = deadline_cloud_settings_widget - # Storage profile should be hidden initially - assert not widget.storage_profile_box.isVisible() - assert not widget.storage_profile_box_label.isVisible() + assert not widget.storage_profile_box.isVisibleTo(widget) + assert not widget.storage_profile_box_label.isVisibleTo(widget) def test_storage_profile_shown_when_profiles_available( @@ -227,12 +209,12 @@ def test_storage_profile_shown_when_profiles_available( """Test that storage profile selector is shown when profiles are available.""" widget = deadline_cloud_settings_widget - # Add a real storage profile item + widget.storage_profile_box.box.addItem("Placeholder", "") widget.storage_profile_box.box.addItem("Test Profile", "sp-test123") - # Storage profile should now be visible - assert widget.storage_profile_box.isVisible() - assert widget.storage_profile_box_label.isVisible() + # Use isVisibleTo(parent) since the widget isn't shown in a window during tests + assert widget.storage_profile_box.isVisibleTo(widget) + assert widget.storage_profile_box_label.isVisibleTo(widget) def test_storage_profile_shown_when_none_selected_sorts_first( @@ -246,22 +228,17 @@ def test_storage_profile_shown_when_none_selected_sorts_first( """ widget = deadline_cloud_settings_widget - # Simulate the async refresh completing with sorted profiles where - # "" sorts first because '<' comes before letters alphabetically items_list = [ ("", ""), ("Profile A", "sp-profile-a"), ("Profile B", "sp-profile-b"), ] - # Emit the _list_update signal to simulate async refresh completing - # This is the code path that was broken - the combo box is populated - # inside block_signals so model signals don't fire - widget.storage_profile_box._list_update.emit(1, items_list) + # Emit with refresh_id=0 to match the widget's initial __refresh_id + widget.storage_profile_box._list_update.emit(0, items_list) - # Storage profile should be visible because there are real profiles - assert widget.storage_profile_box.isVisible() - assert widget.storage_profile_box_label.isVisible() + assert widget.storage_profile_box.isVisibleTo(widget) + assert widget.storage_profile_box_label.isVisibleTo(widget) def test_storage_profile_selection_updates_config( @@ -271,16 +248,11 @@ def test_storage_profile_selection_updates_config( widget = deadline_cloud_settings_widget test_profile_id = "sp-test123" - # Add a test item to the storage profile combo box + widget.storage_profile_box.box.addItem("Default Profile", "sp-default") widget.storage_profile_box.box.addItem("Test Profile", test_profile_id) - with patch( - "deadline.client.ui.widgets.shared_job_settings_tab.set_setting" - ) as mock_set_setting: - # Select the storage profile (triggers _on_storage_profile_changed) - widget.storage_profile_box.box.setCurrentIndex(widget.storage_profile_box.box.count() - 1) - - # Verify config was updated with the storage profile ID + with patch(MOCK_SET_SETTING_PATH) as mock_set_setting: + widget.storage_profile_box.box.setCurrentIndex(1) mock_set_setting.assert_called_with("settings.storage_profile_id", test_profile_id) @@ -289,18 +261,15 @@ def test_queue_change_refreshes_storage_profile_list( ): """Test that changing queue triggers storage profile list refresh.""" widget = deadline_cloud_settings_widget - test_queue_id = "queue-test789" - - # Add a test item to the queue combo box - widget.queue_box.box.addItem("Test Queue", test_queue_id) - with patch.object(widget.storage_profile_box, "refresh_list") as mock_refresh: - with patch("deadline.client.ui.widgets.shared_job_settings_tab.set_setting"): - # Select the queue - widget.queue_box.box.setCurrentIndex(widget.queue_box.box.count() - 1) + widget.queue_box.box.addItem("Default Queue", "queue-default") + widget.queue_box.box.addItem("Test Queue", "queue-test789") - # Verify storage profile list was refreshed - mock_refresh.assert_called_once() + with patch.object(widget.storage_profile_box, "refresh_list") as mock_refresh, patch( + MOCK_SET_SETTING_PATH + ): + widget.queue_box.box.setCurrentIndex(1) + mock_refresh.assert_called_once() # Tests for SharedJobSettingsWidget.refresh_queue_parameters @@ -392,3 +361,75 @@ def test_refresh_queue_parameters_no_refresh_when_unchanged( mock_rebuild.assert_not_called() mock_start_thread.assert_not_called() + + +# Tests verifying refactored module locations and public API surface + + +def test_combo_boxes_importable_from_new_module(): + """Verify combo box classes are importable from their new dedicated module.""" + from deadline.client.ui.widgets.deadline_cloud_resource_combo_boxes import ( + DeadlineFarmListComboBox, + DeadlineQueueListComboBox, + DeadlineStorageProfileNameListComboBox, + ) + + assert DeadlineFarmListComboBox is not None + assert DeadlineQueueListComboBox is not None + assert DeadlineStorageProfileNameListComboBox is not None + + +def test_deadline_cloud_settings_widget_importable_from_widgets_package(): + """Verify DeadlineCloudSettingsWidget is importable from the public widgets package.""" + from deadline.client.ui.widgets import DeadlineCloudSettingsWidget as WidgetFromPackage + from deadline.client.ui.widgets.shared_job_settings_tab import ( + DeadlineCloudSettingsWidget as WidgetFromModule, + ) + + assert WidgetFromPackage is WidgetFromModule + + +def test_farm_change_propagates_config_to_all_boxes( + deadline_cloud_settings_widget: DeadlineCloudSettingsWidget, +): + """Test that changing farm propagates updated config to farm, queue, and storage profile boxes.""" + widget = deadline_cloud_settings_widget + + widget.farm_box.box.addItem("Default Farm", "farm-default") + widget.farm_box.box.addItem("Test Farm", "farm-test") + + with patch(MOCK_SET_SETTING_PATH), patch.object( + widget.farm_box, "set_config" + ) as mock_farm_cfg, patch.object( + widget.queue_box, "set_config" + ) as mock_queue_cfg, patch.object( + widget.storage_profile_box, "set_config" + ) as mock_sp_cfg, patch.object(widget.queue_box, "refresh_list"): + widget.farm_box.box.setCurrentIndex(1) + + mock_farm_cfg.assert_called_once() + mock_queue_cfg.assert_called_once() + mock_sp_cfg.assert_called_once() + + +def test_queue_change_propagates_config_to_all_boxes( + deadline_cloud_settings_widget: DeadlineCloudSettingsWidget, +): + """Test that changing queue propagates updated config to farm, queue, and storage profile boxes.""" + widget = deadline_cloud_settings_widget + + widget.queue_box.box.addItem("Default Queue", "queue-default") + widget.queue_box.box.addItem("Test Queue", "queue-test") + + with patch(MOCK_SET_SETTING_PATH), patch.object( + widget.farm_box, "set_config" + ) as mock_farm_cfg, patch.object( + widget.queue_box, "set_config" + ) as mock_queue_cfg, patch.object( + widget.storage_profile_box, "set_config" + ) as mock_sp_cfg, patch.object(widget.storage_profile_box, "refresh_list"): + widget.queue_box.box.setCurrentIndex(1) + + mock_farm_cfg.assert_called_once() + mock_queue_cfg.assert_called_once() + mock_sp_cfg.assert_called_once() From 541ac73629b20c54086c0fb9776874c864adeb22 Mon Sep 17 00:00:00 2001 From: Louise Fox <208544511+folouiseAWS@users.noreply.github.com> Date: Thu, 12 Feb 2026 10:44:44 -0800 Subject: [PATCH 06/12] refactor: remove dead code and fix type ignores in UI modules - Remove unused refresh_lists() method and on_auth_status_update() from DeadlineWorkstationConfigWidget (functionality moved to DeadlineCloudSettingsWidget) - Fix test import to use correct module for _DeadlineResourceListComboBox - Restore refresh_selected_id() behavior to show raw ID when not in list - Remove type: ignore[union-attr] comments by properly handling None cases - Fix potential IndexError in cli_job_submitter when no QMainWindow exists Signed-off-by: Louise Fox <208544511+folouiseAWS@users.noreply.github.com> --- src/deadline/client/ui/cli_job_submitter.py | 6 +++++- .../client/ui/dialogs/deadline_config_dialog.py | 17 +++-------------- .../client/ui/dialogs/deadline_login_dialog.py | 4 +++- .../ui/dialogs/submit_job_progress_dialog.py | 4 +++- .../ui/dialogs/submit_job_to_deadline_dialog.py | 4 +++- .../deadline_cloud_resource_combo_boxes.py | 9 ++++++++- .../ui/widgets/openjd_parameters_widget.py | 4 +++- .../ui/dialogs/test_deadline_config_dialog.py | 10 ++++++---- 8 files changed, 34 insertions(+), 24 deletions(-) diff --git a/src/deadline/client/ui/cli_job_submitter.py b/src/deadline/client/ui/cli_job_submitter.py index 0577d4fd6..b20168c01 100644 --- a/src/deadline/client/ui/cli_job_submitter.py +++ b/src/deadline/client/ui/cli_job_submitter.py @@ -45,7 +45,11 @@ def show_cli_job_submitter(parent: Optional[QWidget] = None, f=Qt.WindowFlags()) if parent is None: # Get the main application window so we can parent ours to it app = QApplication.instance() - parent = [widget for widget in app.topLevelWidgets() if isinstance(widget, QMainWindow)][0] # type: ignore[union-attr] + if app: + main_windows = [ + widget for widget in app.topLevelWidgets() if isinstance(widget, QMainWindow) + ] + parent = main_windows[0] if main_windows else None def on_create_job_bundle_callback( widget: SubmitJobToDeadlineDialog, diff --git a/src/deadline/client/ui/dialogs/deadline_config_dialog.py b/src/deadline/client/ui/dialogs/deadline_config_dialog.py index 067f0078d..49e0b2c60 100644 --- a/src/deadline/client/ui/dialogs/deadline_config_dialog.py +++ b/src/deadline/client/ui/dialogs/deadline_config_dialog.py @@ -147,9 +147,6 @@ def _build_ui(self): self.auth_status_box.login_clicked.connect(self.on_login) self.layout.addWidget(self.button_box) - # Refresh the lists so queue/farm show the description instead of the ID - self.config_box.refresh_lists() - @property def changes_were_applied(self) -> bool: return self.config_box.changes_were_applied @@ -183,12 +180,9 @@ def on_refresh(self): self.deadline_authentication_status.set_config(self.config_box.config) def on_auth_status_update(self): - # If the AWS Deadline Cloud API is authorized successfully for the AWS profile - # in the config dialog, refresh the farm/queue lists - if self.deadline_authentication_status.api_availability and config_file.get_setting( - "defaults.aws_profile_name", self.deadline_authentication_status.config - ) == config_file.get_setting("defaults.aws_profile_name", self.config_box.config): - self.config_box.refresh_lists() + # Farm, queue, and storage profile lists are now managed by + # DeadlineCloudSettingsWidget in the Submit Dialog, not here. + pass class DeadlineScrollArea(QScrollArea): @@ -683,11 +677,6 @@ def _fill_aws_profiles_box(self): with block_signals(self.aws_profiles_box): self.aws_profiles_box.addItems(list(self.aws_profile_names)) - def refresh_lists(self): - # Farm, queue, and storage profile lists are now managed by - # DeadlineCloudSettingsWidget in the Submit Dialog, not here. - pass - def refresh(self): """ Refreshes all the configuration UI elements from the current config. diff --git a/src/deadline/client/ui/dialogs/deadline_login_dialog.py b/src/deadline/client/ui/dialogs/deadline_login_dialog.py index c46ed4d3f..08c7786d7 100644 --- a/src/deadline/client/ui/dialogs/deadline_login_dialog.py +++ b/src/deadline/client/ui/dialogs/deadline_login_dialog.py @@ -180,8 +180,10 @@ def on_button_clicked(self, button): # Tell the login thread to cancel, then wait for it. self.canceled = True if self.__login_thread: + app = QApplication.instance() while self.__login_thread.is_alive(): - QApplication.instance().processEvents() # type: ignore[union-attr] + if app: + app.processEvents() def exec_(self) -> bool: """ diff --git a/src/deadline/client/ui/dialogs/submit_job_progress_dialog.py b/src/deadline/client/ui/dialogs/submit_job_progress_dialog.py index 1580b1b9d..96ce73107 100644 --- a/src/deadline/client/ui/dialogs/submit_job_progress_dialog.py +++ b/src/deadline/client/ui/dialogs/submit_job_progress_dialog.py @@ -325,8 +325,10 @@ def closeEvent(self, event: QCloseEvent) -> None: logger.info("Canceling submission...") self.status_label.setText(tr("Canceling submission...")) if self.__submission_thread is not None: + app = QApplication.instance() while self.__submission_thread.is_alive(): - QApplication.instance().processEvents() # type: ignore[union-attr] + if app: + app.processEvents() super().closeEvent(event) def exec_(self) -> Optional[str]: # type: ignore[override] diff --git a/src/deadline/client/ui/dialogs/submit_job_to_deadline_dialog.py b/src/deadline/client/ui/dialogs/submit_job_to_deadline_dialog.py index dbfa7d576..505cfe7a9 100644 --- a/src/deadline/client/ui/dialogs/submit_job_to_deadline_dialog.py +++ b/src/deadline/client/ui/dialogs/submit_job_to_deadline_dialog.py @@ -557,7 +557,9 @@ def on_submit(self): job_progress_dialog.progress_window_closed.connect(self._close_event_receiver) job_progress_dialog.setModal(True) job_progress_dialog.show() - QApplication.instance().processEvents() # type: ignore[union-attr] + app = QApplication.instance() + if app: + app.processEvents() # Submit the job try: diff --git a/src/deadline/client/ui/widgets/deadline_cloud_resource_combo_boxes.py b/src/deadline/client/ui/widgets/deadline_cloud_resource_combo_boxes.py index ea77eb817..8b2a2111f 100644 --- a/src/deadline/client/ui/widgets/deadline_cloud_resource_combo_boxes.py +++ b/src/deadline/client/ui/widgets/deadline_cloud_resource_combo_boxes.py @@ -138,14 +138,21 @@ def refresh_selected_id(self): index = self.box.findData(selected_id) if index >= 0: self.box.setCurrentIndex(index) + elif selected_id: + # User has a configured ID but it's not in the list (e.g., lacks + # permission to list resources). Show the raw ID so they can see + # what's configured. + self.box.insertItem(0, selected_id, userData=selected_id) + self.box.setCurrentIndex(0) else: - # Some cases allow to select "nothing" and insert an item to indicate such + # No ID configured - show "" index = self.box.findText("") if index >= 0: self.box.setCurrentIndex(index) else: self.box.insertItem(0, "", userData="") self.box.setCurrentIndex(0) + self.box.setCurrentIndex(0) def _refresh_thread_function(self, refresh_id: int, config: Optional[ConfigParser] = None): """ diff --git a/src/deadline/client/ui/widgets/openjd_parameters_widget.py b/src/deadline/client/ui/widgets/openjd_parameters_widget.py index 49db5bc14..bd3e81c39 100644 --- a/src/deadline/client/ui/widgets/openjd_parameters_widget.py +++ b/src/deadline/client/ui/widgets/openjd_parameters_widget.py @@ -165,7 +165,9 @@ def rebuild_ui( group_layout = _JobTemplateGroupLayout(self, group_label) group_layout.setObjectName(group_label) layout.addWidget(group_layout) - group_layout.layout().addWidget(control) # type: ignore[union-attr] + group_inner_layout = group_layout.layout() + if group_inner_layout: + group_inner_layout.addWidget(control) else: layout.addWidget(control) diff --git a/test/unit/deadline_client/ui/dialogs/test_deadline_config_dialog.py b/test/unit/deadline_client/ui/dialogs/test_deadline_config_dialog.py index 3bc6e3bb6..1dee8b956 100644 --- a/test/unit/deadline_client/ui/dialogs/test_deadline_config_dialog.py +++ b/test/unit/deadline_client/ui/dialogs/test_deadline_config_dialog.py @@ -6,15 +6,17 @@ import pytest try: - from deadline.client.ui.dialogs.deadline_config_dialog import _DeadlineResourceListComboBox + from deadline.client.ui.widgets.deadline_cloud_resource_combo_boxes import ( + _DeadlineResourceListComboBox, + ) except ImportError: - pytest.importorskip("deadline.client.ui.dialogs.deadline_config_dialog") + pytest.importorskip("deadline.client.ui.widgets.deadline_cloud_resource_combo_boxes") class TestDeadlineResourceListComboBox: """Tests for _DeadlineResourceListComboBox.refresh_selected_id()""" - @patch("deadline.client.ui.dialogs.deadline_config_dialog.config_file") + @patch("deadline.client.ui.widgets.deadline_cloud_resource_combo_boxes.config_file") def test_shows_id_when_not_in_list(self, mock_config_file, qtbot): """ When user has a configured ID but lacks permission to list resources, @@ -34,7 +36,7 @@ def test_shows_id_when_not_in_list(self, mock_config_file, qtbot): assert widget.box.currentText() == "farm-abc123" assert widget.box.currentData() == "farm-abc123" - @patch("deadline.client.ui.dialogs.deadline_config_dialog.config_file") + @patch("deadline.client.ui.widgets.deadline_cloud_resource_combo_boxes.config_file") def test_shows_none_selected_when_no_id_configured(self, mock_config_file, qtbot): """When no ID is configured, should show ''.""" mock_config_file.get_setting.return_value = "" From 6928194da2d491f596cd9aeef3f06511c90d9713 Mon Sep 17 00:00:00 2001 From: Louise Fox <208544511+folouiseAWS@users.noreply.github.com> Date: Thu, 12 Feb 2026 11:21:00 -0800 Subject: [PATCH 07/12] fix: correct QComboBox API usage and add storage profile refresh on farm change - Fix insertItem/addItem calls to use positional args instead of userData keyword - Remove duplicate setCurrentIndex call - Add storage_profile_box.refresh_list() when farm changes to match old behavior - Update test fixture to mock config_file.get_setting in combo box module - Update tests to mock storage_profile_box methods Signed-off-by: Louise Fox <208544511+folouiseAWS@users.noreply.github.com> --- .../deadline_cloud_resource_combo_boxes.py | 11 ++--- .../ui/widgets/shared_job_settings_tab.py | 1 + .../widgets/test_shared_job_settings_tab.py | 45 ++++++++++++++----- 3 files changed, 40 insertions(+), 17 deletions(-) diff --git a/src/deadline/client/ui/widgets/deadline_cloud_resource_combo_boxes.py b/src/deadline/client/ui/widgets/deadline_cloud_resource_combo_boxes.py index 8b2a2111f..e2cce0838 100644 --- a/src/deadline/client/ui/widgets/deadline_cloud_resource_combo_boxes.py +++ b/src/deadline/client/ui/widgets/deadline_cloud_resource_combo_boxes.py @@ -109,7 +109,7 @@ def refresh_list(self): # Reset to a list of just the currently configured id during refresh with block_signals(self.box): self.box.clear() - self.box.addItem("", userData=selected_id) + self.box.addItem("", selected_id) self.__refresh_id += 1 self.__refresh_thread = threading.Thread( @@ -125,7 +125,7 @@ def handle_list_update(self, refresh_id, items_list): with block_signals(self.box): self.box.clear() for name, id in items_list: - self.box.addItem(name, userData=id) + self.box.addItem(name, id) self.refresh_selected_id() @@ -142,7 +142,8 @@ def refresh_selected_id(self): # User has a configured ID but it's not in the list (e.g., lacks # permission to list resources). Show the raw ID so they can see # what's configured. - self.box.insertItem(0, selected_id, userData=selected_id) + self.box.insertItem(0, str(selected_id)) + self.box.setItemData(0, selected_id) self.box.setCurrentIndex(0) else: # No ID configured - show "" @@ -150,8 +151,8 @@ def refresh_selected_id(self): if index >= 0: self.box.setCurrentIndex(index) else: - self.box.insertItem(0, "", userData="") - self.box.setCurrentIndex(0) + self.box.insertItem(0, "") + self.box.setItemData(0, "") self.box.setCurrentIndex(0) def _refresh_thread_function(self, refresh_id: int, config: Optional[ConfigParser] = None): diff --git a/src/deadline/client/ui/widgets/shared_job_settings_tab.py b/src/deadline/client/ui/widgets/shared_job_settings_tab.py index 61154b4ad..d0d616881 100644 --- a/src/deadline/client/ui/widgets/shared_job_settings_tab.py +++ b/src/deadline/client/ui/widgets/shared_job_settings_tab.py @@ -566,6 +566,7 @@ def _on_farm_changed(self, index: int): set_setting("defaults.farm_id", farm_id) self._update_all_box_configs() self.queue_box.refresh_list() + self.storage_profile_box.refresh_list() self._notify_parent_refresh() def _on_queue_changed(self, index: int): diff --git a/test/unit/deadline_client/ui/widgets/test_shared_job_settings_tab.py b/test/unit/deadline_client/ui/widgets/test_shared_job_settings_tab.py index d3fa892b8..35256eee6 100644 --- a/test/unit/deadline_client/ui/widgets/test_shared_job_settings_tab.py +++ b/test/unit/deadline_client/ui/widgets/test_shared_job_settings_tab.py @@ -92,13 +92,19 @@ def test_max_worker_count_should_be_integer_within_range( MOCK_CONFIG_PATH = "deadline.client.ui.widgets.shared_job_settings_tab.config_file.read_config" MOCK_SET_SETTING_PATH = "deadline.client.ui.widgets.shared_job_settings_tab.set_setting" +MOCK_COMBO_BOX_GET_SETTING_PATH = ( + "deadline.client.ui.widgets.deadline_cloud_resource_combo_boxes.config_file.get_setting" +) @pytest.fixture(scope="function") def deadline_cloud_settings_widget(qtbot) -> Generator[DeadlineCloudSettingsWidget, None, None]: """Fixture for DeadlineCloudSettingsWidget with mocked config.""" - with patch(MOCK_CONFIG_PATH) as mock_config: + with patch(MOCK_CONFIG_PATH) as mock_config, patch( + MOCK_COMBO_BOX_GET_SETTING_PATH + ) as mock_get_setting: mock_config.return_value = MagicMock() + mock_get_setting.return_value = "" # Return empty string for all get_setting calls widget = DeadlineCloudSettingsWidget() qtbot.addWidget(widget) yield widget @@ -136,42 +142,52 @@ def test_queue_selection_updates_config( def test_farm_change_refreshes_queue_list( deadline_cloud_settings_widget: DeadlineCloudSettingsWidget, ): - """Test that changing farm triggers queue list refresh.""" + """Test that changing farm triggers queue and storage profile list refresh.""" widget = deadline_cloud_settings_widget widget.farm_box.box.addItem("Default Farm", "farm-default") widget.farm_box.box.addItem("Test Farm", "farm-test789") - with patch.object(widget.queue_box, "refresh_list") as mock_refresh, patch( - MOCK_SET_SETTING_PATH - ): + with patch.object(widget.queue_box, "refresh_list") as mock_queue_refresh, patch.object( + widget.storage_profile_box, "refresh_list" + ) as mock_sp_refresh, patch(MOCK_SET_SETTING_PATH): widget.farm_box.box.setCurrentIndex(1) - mock_refresh.assert_called_once() + mock_queue_refresh.assert_called_once() + mock_sp_refresh.assert_called_once() def test_refresh_setting_controls_updates_combo_boxes( deadline_cloud_settings_widget: DeadlineCloudSettingsWidget, ): - """Test that refresh_setting_controls updates both combo boxes.""" + """Test that refresh_setting_controls updates all combo boxes.""" widget = deadline_cloud_settings_widget with patch.object(widget.farm_box, "set_config") as mock_farm_config, patch.object( widget.queue_box, "set_config" ) as mock_queue_config, patch.object( + widget.storage_profile_box, "set_config" + ) as mock_sp_config, patch.object( widget.farm_box, "refresh_selected_id" ) as mock_farm_refresh, patch.object( widget.queue_box, "refresh_selected_id" ) as mock_queue_refresh, patch.object( + widget.storage_profile_box, "refresh_selected_id" + ) as mock_sp_refresh, patch.object( widget.farm_box, "refresh_list" - ) as mock_farm_list, patch.object(widget.queue_box, "refresh_list") as mock_queue_list: + ) as mock_farm_list, patch.object( + widget.queue_box, "refresh_list" + ) as mock_queue_list, patch.object(widget.storage_profile_box, "refresh_list") as mock_sp_list: widget.refresh_setting_controls(deadline_authorized=True) mock_farm_config.assert_called_once() mock_queue_config.assert_called_once() + mock_sp_config.assert_called_once() mock_farm_refresh.assert_called_once() mock_queue_refresh.assert_called_once() + mock_sp_refresh.assert_called_once() mock_farm_list.assert_called_once() mock_queue_list.assert_called_once() + mock_sp_list.assert_called_once() def test_refresh_setting_controls_skips_list_refresh_when_unauthorized( @@ -182,15 +198,18 @@ def test_refresh_setting_controls_skips_list_refresh_when_unauthorized( with patch.object(widget.farm_box, "set_config"), patch.object( widget.queue_box, "set_config" - ), patch.object(widget.farm_box, "refresh_selected_id"), patch.object( - widget.queue_box, "refresh_selected_id" + ), patch.object(widget.storage_profile_box, "set_config"), patch.object( + widget.farm_box, "refresh_selected_id" + ), patch.object(widget.queue_box, "refresh_selected_id"), patch.object( + widget.storage_profile_box, "refresh_selected_id" ), patch.object(widget.farm_box, "refresh_list") as mock_farm_list, patch.object( widget.queue_box, "refresh_list" - ) as mock_queue_list: + ) as mock_queue_list, patch.object(widget.storage_profile_box, "refresh_list") as mock_sp_list: widget.refresh_setting_controls(deadline_authorized=False) mock_farm_list.assert_not_called() mock_queue_list.assert_not_called() + mock_sp_list.assert_not_called() def test_storage_profile_hidden_by_default( @@ -404,7 +423,9 @@ def test_farm_change_propagates_config_to_all_boxes( widget.queue_box, "set_config" ) as mock_queue_cfg, patch.object( widget.storage_profile_box, "set_config" - ) as mock_sp_cfg, patch.object(widget.queue_box, "refresh_list"): + ) as mock_sp_cfg, patch.object(widget.queue_box, "refresh_list"), patch.object( + widget.storage_profile_box, "refresh_list" + ): widget.farm_box.box.setCurrentIndex(1) mock_farm_cfg.assert_called_once() From 03c56a647ccf4ffb0e8c13ec4a12949370cb4bfb Mon Sep 17 00:00:00 2001 From: Louise Fox <208544511+folouiseAWS@users.noreply.github.com> Date: Thu, 12 Feb 2026 13:08:08 -0800 Subject: [PATCH 08/12] refactor: make list_update signal public in combo boxes - Rename _list_update to list_update for proper public API - Add comment explaining lambda discards signal args intentionally - Update test to use new public signal name Signed-off-by: Louise Fox <208544511+folouiseAWS@users.noreply.github.com> --- .../ui/widgets/deadline_cloud_resource_combo_boxes.py | 10 +++++----- .../client/ui/widgets/shared_job_settings_tab.py | 5 +++-- .../ui/widgets/test_shared_job_settings_tab.py | 6 +++--- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/deadline/client/ui/widgets/deadline_cloud_resource_combo_boxes.py b/src/deadline/client/ui/widgets/deadline_cloud_resource_combo_boxes.py index e2cce0838..68cb4c675 100644 --- a/src/deadline/client/ui/widgets/deadline_cloud_resource_combo_boxes.py +++ b/src/deadline/client/ui/widgets/deadline_cloud_resource_combo_boxes.py @@ -47,9 +47,9 @@ class _DeadlineResourceListComboBox(QWidget): # provides (operation_name, BaseException) background_exception = Signal(str, BaseException) - # Emitted when an async refresh_farms_list thread completes, - # provides (refresh_id, [(farm_id, farm_name), ...]) - _list_update = Signal(int, list) + # Emitted when an async refresh list thread completes, + # provides (refresh_id, [(resource_id, resource_name), ...]) + list_update = Signal(int, list) def __init__(self, resource_name, setting_name, parent: Optional[QWidget] = None): super().__init__(parent) @@ -75,7 +75,7 @@ def _build_ui(self): self.refresh_button.setIcon(QApplication.style().standardIcon(QStyle.SP_BrowserReload)) self.refresh_button.setFixedSize(QSize(22, 22)) # Make the button square self.refresh_button.clicked.connect(self.refresh_list) - self._list_update.connect(self.handle_list_update) + self.list_update.connect(self.handle_list_update) self.background_exception.connect(self.handle_background_exception) def handle_background_exception(self, e): @@ -162,7 +162,7 @@ def _refresh_thread_function(self, refresh_id: int, config: Optional[ConfigParse try: resources = self.list_resources(config=config) if not self.canceled: - self._list_update.emit(refresh_id, resources) + self.list_update.emit(refresh_id, resources) except BaseException as e: if not self.canceled and refresh_id == self.__refresh_id: self.background_exception.emit(f"Refresh {self.resource_name}s list", e) diff --git a/src/deadline/client/ui/widgets/shared_job_settings_tab.py b/src/deadline/client/ui/widgets/shared_job_settings_tab.py index d0d616881..747ad8885 100644 --- a/src/deadline/client/ui/widgets/shared_job_settings_tab.py +++ b/src/deadline/client/ui/widgets/shared_job_settings_tab.py @@ -521,8 +521,9 @@ def _build_ui(self): self.storage_profile_box.box.model().modelReset.connect( self._update_storage_profile_visibility ) - # Also connect to the _list_update signal to handle updates after async refresh - self.storage_profile_box._list_update.connect( + # Also connect to the list_update signal to handle updates after async refresh + # Args (refresh_id, items_list) not needed - just trigger visibility update + self.storage_profile_box.list_update.connect( lambda *args: self._update_storage_profile_visibility() ) diff --git a/test/unit/deadline_client/ui/widgets/test_shared_job_settings_tab.py b/test/unit/deadline_client/ui/widgets/test_shared_job_settings_tab.py index 35256eee6..8f956e0da 100644 --- a/test/unit/deadline_client/ui/widgets/test_shared_job_settings_tab.py +++ b/test/unit/deadline_client/ui/widgets/test_shared_job_settings_tab.py @@ -241,8 +241,8 @@ def test_storage_profile_shown_when_none_selected_sorts_first( ): """Test that storage profile is visible even when sorts to first position. - This tests the _list_update signal path which is used during async refresh. - The _list_update signal handler populates the combo box with block_signals, + This tests the list_update signal path which is used during async refresh. + The list_update signal handler populates the combo box with block_signals, so model signals don't fire - we need to explicitly trigger visibility update. """ widget = deadline_cloud_settings_widget @@ -254,7 +254,7 @@ def test_storage_profile_shown_when_none_selected_sorts_first( ] # Emit with refresh_id=0 to match the widget's initial __refresh_id - widget.storage_profile_box._list_update.emit(0, items_list) + widget.storage_profile_box.list_update.emit(0, items_list) assert widget.storage_profile_box.isVisibleTo(widget) assert widget.storage_profile_box_label.isVisibleTo(widget) From 47c8b8467886b2d65d01f278e7aca220cdcd7a24 Mon Sep 17 00:00:00 2001 From: Louise Fox <208544511+folouiseAWS@users.noreply.github.com> Date: Thu, 12 Feb 2026 13:24:22 -0800 Subject: [PATCH 09/12] fix: restore userData keyword args and block_signals in combo boxes - Restore userData= keyword arg pattern for QComboBox.insertItem and addItem calls in _DeadlineResourceListComboBox. The two-arg setItemData() form writes to Qt.DisplayRole instead of Qt.UserRole, causing findData() to fail to locate items. - Add block_signals around programmatic combo box updates in DeadlineCloudSettingsWidget.refresh_setting_controls to prevent currentIndexChanged from firing during repopulation and spuriously writing to the global config. - Remove dead on_auth_status_update method and its signal connection from DeadlineConfigDialog. Farm/queue/storage profile refresh is now handled by DeadlineCloudSettingsWidget in the Submit Dialog. Signed-off-by: Louise Fox <208544511+folouiseAWS@users.noreply.github.com> --- .../client/ui/dialogs/deadline_config_dialog.py | 8 -------- .../deadline_cloud_resource_combo_boxes.py | 10 ++++------ .../client/ui/widgets/shared_job_settings_tab.py | 16 ++++++++++------ 3 files changed, 14 insertions(+), 20 deletions(-) diff --git a/src/deadline/client/ui/dialogs/deadline_config_dialog.py b/src/deadline/client/ui/dialogs/deadline_config_dialog.py index 49e0b2c60..eb9a0e4c8 100644 --- a/src/deadline/client/ui/dialogs/deadline_config_dialog.py +++ b/src/deadline/client/ui/dialogs/deadline_config_dialog.py @@ -129,9 +129,6 @@ def _build_ui(self): ) self.layout.addWidget(self.auth_status_box) self.deadline_authentication_status.deadline_config_changed.connect(self.config_box.refresh) - self.deadline_authentication_status.api_availability_changed.connect( - self.on_auth_status_update - ) # We only use a Close button, not OK/Cancel, because we live update the settings. self.button_box = QDialogButtonBox( @@ -179,11 +176,6 @@ def on_refresh(self): # Update the auth status with the refreshed config self.deadline_authentication_status.set_config(self.config_box.config) - def on_auth_status_update(self): - # Farm, queue, and storage profile lists are now managed by - # DeadlineCloudSettingsWidget in the Submit Dialog, not here. - pass - class DeadlineScrollArea(QScrollArea): def __init__(self, parent: Optional[QWidget] = None): diff --git a/src/deadline/client/ui/widgets/deadline_cloud_resource_combo_boxes.py b/src/deadline/client/ui/widgets/deadline_cloud_resource_combo_boxes.py index 68cb4c675..d31f5da55 100644 --- a/src/deadline/client/ui/widgets/deadline_cloud_resource_combo_boxes.py +++ b/src/deadline/client/ui/widgets/deadline_cloud_resource_combo_boxes.py @@ -109,7 +109,7 @@ def refresh_list(self): # Reset to a list of just the currently configured id during refresh with block_signals(self.box): self.box.clear() - self.box.addItem("", selected_id) + self.box.addItem("", userData=selected_id) self.__refresh_id += 1 self.__refresh_thread = threading.Thread( @@ -125,7 +125,7 @@ def handle_list_update(self, refresh_id, items_list): with block_signals(self.box): self.box.clear() for name, id in items_list: - self.box.addItem(name, id) + self.box.addItem(name, userData=id) self.refresh_selected_id() @@ -142,8 +142,7 @@ def refresh_selected_id(self): # User has a configured ID but it's not in the list (e.g., lacks # permission to list resources). Show the raw ID so they can see # what's configured. - self.box.insertItem(0, str(selected_id)) - self.box.setItemData(0, selected_id) + self.box.insertItem(0, selected_id, userData=selected_id) self.box.setCurrentIndex(0) else: # No ID configured - show "" @@ -151,8 +150,7 @@ def refresh_selected_id(self): if index >= 0: self.box.setCurrentIndex(index) else: - self.box.insertItem(0, "") - self.box.setItemData(0, "") + self.box.insertItem(0, "", userData="") self.box.setCurrentIndex(0) def _refresh_thread_function(self, refresh_id: int, config: Optional[ConfigParser] = None): diff --git a/src/deadline/client/ui/widgets/shared_job_settings_tab.py b/src/deadline/client/ui/widgets/shared_job_settings_tab.py index 747ad8885..7932024ed 100644 --- a/src/deadline/client/ui/widgets/shared_job_settings_tab.py +++ b/src/deadline/client/ui/widgets/shared_job_settings_tab.py @@ -23,7 +23,7 @@ ) from ...config import get_setting, set_setting, config_file -from .._utils import CancelationFlag, tr +from .._utils import CancelationFlag, block_signals, tr from .openjd_parameters_widget import OpenJDParametersWidget from ...api import get_queue_parameter_definitions from .deadline_cloud_resource_combo_boxes import ( @@ -630,12 +630,16 @@ def refresh_setting_controls(self, deadline_authorized): """ self._update_all_box_configs() - # Refresh selected items to reflect current config - self.farm_box.refresh_selected_id() - self.queue_box.refresh_selected_id() - self.storage_profile_box.refresh_selected_id() + # Use block_signals to prevent currentIndexChanged from firing during + # programmatic updates, which would spuriously write to config. + with block_signals(self.farm_box.box), block_signals(self.queue_box.box), block_signals( + self.storage_profile_box.box + ): + self.farm_box.refresh_selected_id() + self.queue_box.refresh_selected_id() + self.storage_profile_box.refresh_selected_id() - # Refresh lists if authorized + # Refresh lists if authorized (refresh_list already uses block_signals internally) if deadline_authorized: self.farm_box.refresh_list() self.queue_box.refresh_list() From d679ee4d3b223853f0e471a1c6dc9b9fd79bcdbd Mon Sep 17 00:00:00 2001 From: Louise Fox <208544511+folouiseAWS@users.noreply.github.com> Date: Thu, 12 Feb 2026 13:40:18 -0800 Subject: [PATCH 10/12] fix: remove duplicate refresh_list calls and simplify parent traversal - Remove direct refresh_list() calls from _on_farm_changed and _on_queue_changed since _notify_parent_refresh already triggers refresh_deadline_settings which calls refresh_setting_controls, eliminating duplicate background API calls. - Simplify _find_parent_with_attr to use parent.parent() directly instead of redundant hasattr/callable guard. - Update tests to reflect the refactored refresh flow. Signed-off-by: Louise Fox <208544511+folouiseAWS@users.noreply.github.com> --- .../ui/widgets/shared_job_settings_tab.py | 11 +++---- .../widgets/test_shared_job_settings_tab.py | 31 +++++++------------ 2 files changed, 17 insertions(+), 25 deletions(-) diff --git a/src/deadline/client/ui/widgets/shared_job_settings_tab.py b/src/deadline/client/ui/widgets/shared_job_settings_tab.py index 7932024ed..953755b44 100644 --- a/src/deadline/client/ui/widgets/shared_job_settings_tab.py +++ b/src/deadline/client/ui/widgets/shared_job_settings_tab.py @@ -566,8 +566,9 @@ def _on_farm_changed(self, index: int): set_setting("defaults.farm_id", farm_id) self._update_all_box_configs() - self.queue_box.refresh_list() - self.storage_profile_box.refresh_list() + # Don't call refresh_list() here — _notify_parent_refresh triggers + # refresh_deadline_settings which calls refresh_setting_controls, + # and that already refreshes all lists when authorized. self._notify_parent_refresh() def _on_queue_changed(self, index: int): @@ -581,7 +582,7 @@ def _on_queue_changed(self, index: int): set_setting("defaults.queue_id", queue_id) self._update_all_box_configs() - self.storage_profile_box.refresh_list() + # Don't call refresh_list() here — same reason as _on_farm_changed. self._notify_parent_refresh() def _on_storage_profile_changed(self, index: int): @@ -601,9 +602,7 @@ def _find_parent_with_attr(self, attr_name: str) -> Optional[QWidget]: while parent is not None: if hasattr(parent, attr_name): return parent - parent = ( - parent.parent() if hasattr(parent, "parent") and callable(parent.parent) else None - ) # type: ignore[assignment] + parent = parent.parent() # type: ignore[assignment] return None def _notify_parent_refresh(self): diff --git a/test/unit/deadline_client/ui/widgets/test_shared_job_settings_tab.py b/test/unit/deadline_client/ui/widgets/test_shared_job_settings_tab.py index 8f956e0da..7244b3872 100644 --- a/test/unit/deadline_client/ui/widgets/test_shared_job_settings_tab.py +++ b/test/unit/deadline_client/ui/widgets/test_shared_job_settings_tab.py @@ -139,21 +139,20 @@ def test_queue_selection_updates_config( mock_set_setting.assert_called_with("defaults.queue_id", test_queue_id) -def test_farm_change_refreshes_queue_list( +def test_farm_change_triggers_parent_refresh( deadline_cloud_settings_widget: DeadlineCloudSettingsWidget, ): - """Test that changing farm triggers queue and storage profile list refresh.""" + """Test that changing farm triggers _notify_parent_refresh (which cascades to refresh lists).""" widget = deadline_cloud_settings_widget widget.farm_box.box.addItem("Default Farm", "farm-default") widget.farm_box.box.addItem("Test Farm", "farm-test789") - with patch.object(widget.queue_box, "refresh_list") as mock_queue_refresh, patch.object( - widget.storage_profile_box, "refresh_list" - ) as mock_sp_refresh, patch(MOCK_SET_SETTING_PATH): + with patch.object(widget, "_notify_parent_refresh") as mock_notify, patch( + MOCK_SET_SETTING_PATH + ): widget.farm_box.box.setCurrentIndex(1) - mock_queue_refresh.assert_called_once() - mock_sp_refresh.assert_called_once() + mock_notify.assert_called_once() def test_refresh_setting_controls_updates_combo_boxes( @@ -275,20 +274,20 @@ def test_storage_profile_selection_updates_config( mock_set_setting.assert_called_with("settings.storage_profile_id", test_profile_id) -def test_queue_change_refreshes_storage_profile_list( +def test_queue_change_triggers_parent_refresh( deadline_cloud_settings_widget: DeadlineCloudSettingsWidget, ): - """Test that changing queue triggers storage profile list refresh.""" + """Test that changing queue triggers _notify_parent_refresh (which cascades to refresh lists).""" widget = deadline_cloud_settings_widget widget.queue_box.box.addItem("Default Queue", "queue-default") widget.queue_box.box.addItem("Test Queue", "queue-test789") - with patch.object(widget.storage_profile_box, "refresh_list") as mock_refresh, patch( + with patch.object(widget, "_notify_parent_refresh") as mock_notify, patch( MOCK_SET_SETTING_PATH ): widget.queue_box.box.setCurrentIndex(1) - mock_refresh.assert_called_once() + mock_notify.assert_called_once() # Tests for SharedJobSettingsWidget.refresh_queue_parameters @@ -421,11 +420,7 @@ def test_farm_change_propagates_config_to_all_boxes( widget.farm_box, "set_config" ) as mock_farm_cfg, patch.object( widget.queue_box, "set_config" - ) as mock_queue_cfg, patch.object( - widget.storage_profile_box, "set_config" - ) as mock_sp_cfg, patch.object(widget.queue_box, "refresh_list"), patch.object( - widget.storage_profile_box, "refresh_list" - ): + ) as mock_queue_cfg, patch.object(widget.storage_profile_box, "set_config") as mock_sp_cfg: widget.farm_box.box.setCurrentIndex(1) mock_farm_cfg.assert_called_once() @@ -446,9 +441,7 @@ def test_queue_change_propagates_config_to_all_boxes( widget.farm_box, "set_config" ) as mock_farm_cfg, patch.object( widget.queue_box, "set_config" - ) as mock_queue_cfg, patch.object( - widget.storage_profile_box, "set_config" - ) as mock_sp_cfg, patch.object(widget.storage_profile_box, "refresh_list"): + ) as mock_queue_cfg, patch.object(widget.storage_profile_box, "set_config") as mock_sp_cfg: widget.queue_box.box.setCurrentIndex(1) mock_farm_cfg.assert_called_once() From c72b2d6d03eaf0b4ec68889889b15503bf759035 Mon Sep 17 00:00:00 2001 From: Louise Fox <208544511+folouiseAWS@users.noreply.github.com> Date: Thu, 12 Feb 2026 13:55:44 -0800 Subject: [PATCH 11/12] fix: show error dialog when background resource refresh fails Connect background_exception signals from farm, queue, and storage profile combo boxes to a handler that displays QMessageBox.warning, restoring the error visibility that existed in the old Settings Dialog. Previously, API errors during list refresh were silently swallowed. Signed-off-by: Louise Fox <208544511+folouiseAWS@users.noreply.github.com> --- .../client/ui/widgets/shared_job_settings_tab.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/deadline/client/ui/widgets/shared_job_settings_tab.py b/src/deadline/client/ui/widgets/shared_job_settings_tab.py index 953755b44..1b5507d6d 100644 --- a/src/deadline/client/ui/widgets/shared_job_settings_tab.py +++ b/src/deadline/client/ui/widgets/shared_job_settings_tab.py @@ -16,6 +16,7 @@ QGroupBox, QLabel, QLineEdit, + QMessageBox, QRadioButton, QSpinBox, QVBoxLayout, @@ -533,6 +534,15 @@ def _build_ui(self): self.queue_box.set_config(config) self.storage_profile_box.set_config(config) + # Connect background exception signals to show errors to the user + self.farm_box.background_exception.connect(self._handle_background_exception) + self.queue_box.background_exception.connect(self._handle_background_exception) + self.storage_profile_box.background_exception.connect(self._handle_background_exception) + + def _handle_background_exception(self, title, e): + """Show a warning dialog when a background refresh thread encounters an error.""" + QMessageBox.warning(self, title, f"Encountered an error:\n{e}") # type: ignore[call-arg] + def _set_storage_profile_visible(self, visible: bool): """Show or hide the storage profile selector""" self.storage_profile_box_label.setVisible(visible) From 810855f34e97fa43c705473aeff43d4533da1c85 Mon Sep 17 00:00:00 2001 From: Louise Fox <208544511+folouiseAWS@users.noreply.github.com> Date: Fri, 13 Feb 2026 10:48:47 -0800 Subject: [PATCH 12/12] fix: prevent double refresh cascade on farm/queue selection change Remove duplicate refresh_queue_parameters call from _notify_parent_refresh in SharedJobSettingsWidget - refresh_deadline_settings already calls it. Remove deadline_config_changed signal connection in SubmitJobToDeadlineDialog that caused a second async refresh via QFileSystemWatcher feedback loop when set_setting() writes to disk. The synchronous path via _notify_parent_refresh is sufficient. Signed-off-by: Louise Fox <208544511+folouiseAWS@users.noreply.github.com> --- .../client/ui/dialogs/submit_job_to_deadline_dialog.py | 10 +++++++--- .../client/ui/widgets/shared_job_settings_tab.py | 9 +++------ 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/deadline/client/ui/dialogs/submit_job_to_deadline_dialog.py b/src/deadline/client/ui/dialogs/submit_job_to_deadline_dialog.py index 505cfe7a9..29eb19eae 100644 --- a/src/deadline/client/ui/dialogs/submit_job_to_deadline_dialog.py +++ b/src/deadline/client/ui/dialogs/submit_job_to_deadline_dialog.py @@ -225,9 +225,13 @@ def _build_ui( self.deadline_authentication_status.api_availability_changed.connect( self.refresh_deadline_settings ) - self.deadline_authentication_status.deadline_config_changed.connect( - self.refresh_deadline_settings - ) + # Note: we intentionally do NOT connect deadline_config_changed here. + # Farm/queue/storage profile changes in DeadlineCloudSettingsWidget call + # set_setting() which writes to disk, triggering the QFileSystemWatcher. + # If we connected deadline_config_changed → refresh_deadline_settings, + # every selection change would trigger the full refresh cascade twice + # (once synchronously via _notify_parent_refresh, once asynchronously + # via the file watcher). The synchronous path is sufficient. # Refresh the submit button enable state once queue parameter status changes self.shared_job_settings.valid_parameters.connect(self._set_submit_button_state) diff --git a/src/deadline/client/ui/widgets/shared_job_settings_tab.py b/src/deadline/client/ui/widgets/shared_job_settings_tab.py index 1b5507d6d..d56679584 100644 --- a/src/deadline/client/ui/widgets/shared_job_settings_tab.py +++ b/src/deadline/client/ui/widgets/shared_job_settings_tab.py @@ -617,12 +617,9 @@ def _find_parent_with_attr(self, attr_name: str) -> Optional[QWidget]: def _notify_parent_refresh(self): """Helper to notify parent widgets to refresh after config changes""" - # Find and call refresh_queue_parameters on parent chain - parent = self._find_parent_with_attr("refresh_queue_parameters") - if parent: - parent.refresh_queue_parameters() - - # Find and call refresh_deadline_settings on parent chain + # Find and call refresh_deadline_settings on parent chain. + # refresh_deadline_settings already calls refresh_queue_parameters + # internally, so we don't call it separately here. parent = self._find_parent_with_attr("refresh_deadline_settings") if parent: parent.refresh_deadline_settings()