diff --git a/pyproject.toml b/pyproject.toml index 11e5df2..24775ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ classifiers = [ dependencies = [ - "deadline >= 0.59.0,< 0.60", + "deadline >= 0.60.1,< 0.61", ] [project.scripts] diff --git a/src/deadline/vred_submitter/vred_submitter.py b/src/deadline/vred_submitter/vred_submitter.py index 0a77691..e9a6b7f 100644 --- a/src/deadline/vred_submitter/vred_submitter.py +++ b/src/deadline/vred_submitter/vred_submitter.py @@ -32,6 +32,7 @@ from deadline.client.api import ( get_deadline_cloud_library_telemetry_client, ) +from deadline.client.config import get_setting, str2bool from deadline.client.exceptions import DeadlineOperationError, UserInitiatedCancel from deadline.client.job_bundle._yaml import deadline_yaml_dump from deadline.client.job_bundle.parameters import JobParameter @@ -40,6 +41,12 @@ SubmitJobToDeadlineDialog, JobBundlePurpose, ) +from deadline.client.ui.pre_gui_hooks import ( + apply_pre_gui_output, + PreGuiHookContext, + qt_hook_confirmation, + run_pre_gui_hooks, +) from PySide6.QtCore import Qt @@ -48,6 +55,18 @@ _global_logger = get_logger(__name__) +def _pre_gui_hook_confirm_callback(parent): + """Choose the confirmation callback for pre-GUI hooks based on the auto_accept setting. + + Returns ``None`` (run hooks without prompting) when ``settings.auto_accept`` is enabled, + otherwise the standard Qt confirmation dialog from ``qt_hook_confirmation``. Kept as a small + helper so the auto_accept branch can be unit-tested headlessly. + """ + if str2bool(get_setting("settings.auto_accept")): + return None + return qt_hook_confirmation(parent) + + class VREDSubmitter: def __init__(self, parent_window: Any, window_flags: Qt.WindowFlags = Qt.WindowFlags()): @@ -238,6 +257,24 @@ def _create_submitter_dialog( shared_parameter_values = {Constants.CONDA_PACKAGES_JOB_PARAM: conda_packages} if conda_channels: shared_parameter_values[Constants.CONDA_CHANNELS_JOB_PARAM] = conda_channels + + # Run pre-GUI hooks so studios can pre-populate dialog fields before it opens. VRED has + # no on-disk job bundle at this point, so hooks are sourced from DEADLINE_HOOKS_DIR only + # (bundle_dir=None), gated by settings.allow_environment_hooks. The confirmation prompt is + # skipped when auto_accept is set; otherwise the standard dialog is shown. + pre_gui_output = run_pre_gui_hooks( + PreGuiHookContext( + bundle_dir=None, + job_name=render_settings.name, + submitter_name="vred", + parameters=dict(shared_parameter_values), + ), + confirm_callback=_pre_gui_hook_confirm_callback(self.parent_window), + ) + # RenderSubmitterUISettings has no .parameters list, so apply_pre_gui_output routes every + # hook parameter into shared_parameter_values and writes name/description onto the settings. + apply_pre_gui_output(pre_gui_output, render_settings, shared_parameter_values) + # Need to apply these settings prior in order to ensure that Qt Controls are sized as expected! _global_dpi_scale.factor = get_dpi_scale_factor() submitter_dialog = SubmitJobToDeadlineDialog( diff --git a/test/unit/test_pre_gui_hooks.py b/test/unit/test_pre_gui_hooks.py new file mode 100644 index 0000000..b80f5e0 --- /dev/null +++ b/test/unit/test_pre_gui_hooks.py @@ -0,0 +1,208 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + +"""Unit tests for the VRED submitter's pre-GUI hook integration. + +``VREDSubmitter._create_submitter_dialog`` calls deadline-cloud's ``run_pre_gui_hooks`` +(env-only, since VRED has no on-disk bundle) and then maps the merged output onto its own +``RenderSubmitterUISettings`` + the dialog's shared parameter values via deadline-cloud's +generic ``apply_pre_gui_output``. + +The full submitter needs a running VRED and Qt, so it is exercised in the integration suite. +This module covers the DCC-owned pieces at unit level: + +* ``TestApplyPreGuiOutputForVred`` pins the contract that matters for VRED: its + ``RenderSubmitterUISettings`` has assignable ``name`` / ``description`` and **no** + ``.parameters`` list, so ``apply_pre_gui_output`` must write name/description onto the settings + and route every hook parameter into the shared values dict. It drives the real core function + against the real settings dataclass rather than re-testing core internals. +* ``TestPreGuiHookConfirmCallback`` covers ``_pre_gui_hook_confirm_callback`` — the + ``settings.auto_accept`` branch — including that the returned callback actually fires the Qt + confirmation dialog (via the real ``qt_hook_confirmation``) when auto_accept is disabled. +* ``TestCreateSubmitterDialogPreGuiWiring`` covers the wiring added to + ``_create_submitter_dialog`` — the ``PreGuiHookContext`` construction and that + ``apply_pre_gui_output`` is applied onto ``render_settings`` / the shared values before the + dialog is built — with deadline-cloud mocked. + +``apply_pre_gui_output`` first ships in deadline-cloud 0.60.1 (the floor this change sets); it is +absent from 0.60.0. +""" + +from unittest.mock import Mock, patch + +from deadline.client.ui.pre_gui_hooks import apply_pre_gui_output + +from vred_submitter.data_classes import RenderSubmitterUISettings +from vred_submitter.vred_submitter import _pre_gui_hook_confirm_callback, VREDSubmitter + + +def _settings() -> RenderSubmitterUISettings: + s = RenderSubmitterUISettings() + s.name = "Original" + s.description = "" + return s + + +class TestApplyPreGuiOutputForVred: + """The generic core ``apply_pre_gui_output`` driven against VRED's real settings dataclass.""" + + def test_settings_dataclass_has_no_parameters_list(self): + """The premise for VRED's mapping: RenderSubmitterUISettings has no .parameters list, so + apply_pre_gui_output treats every hook parameter as a shared value.""" + assert not hasattr(RenderSubmitterUISettings(), "parameters") + + def test_name_and_description_applied_to_settings(self): + """A hook's name/description overwrite the settings fields (VRED has no .parameters list, + so these land directly on the dataclass).""" + settings = _settings() + shared = {"CondaPackages": "vredcore=2024*"} + + apply_pre_gui_output( + {"name": "PREGUI RAN", "description": "from pipeline"}, settings, shared + ) + + assert settings.name == "PREGUI RAN" + assert settings.description == "from pipeline" + + def test_hook_parameters_merged_into_shared_values(self): + """With no template-parameter list, all hook parameters (queue params, deadline: + properties) flow into the shared values the dialog is seeded with, overriding defaults on + key collision.""" + settings = _settings() + shared = {"CondaPackages": "vredcore=2024*", "CondaChannels": "deadline-cloud"} + + apply_pre_gui_output( + { + "parameters": { + "deadline:priority": 88, + "CondaPackages": "vredcore=2025* custom_pkg", # overrides the default + } + }, + settings, + shared, + ) + + assert shared["deadline:priority"] == 88 + assert shared["CondaPackages"] == "vredcore=2025* custom_pkg" + assert shared["CondaChannels"] == "deadline-cloud" # untouched keys preserved + + def test_empty_output_is_a_noop(self): + """No pre-GUI hook output leaves the settings and shared values unchanged.""" + settings = _settings() + shared = {"CondaPackages": "pkg"} + + apply_pre_gui_output({}, settings, shared) + + assert settings.name == "Original" + assert settings.description == "" + assert shared == {"CondaPackages": "pkg"} + + def test_partial_output_only_touches_present_keys(self): + """Only the keys present in the output are applied; others keep their prior values.""" + settings = _settings() + settings.description = "keep me" + shared: dict = {} + + apply_pre_gui_output({"name": "NewName"}, settings, shared) + + assert settings.name == "NewName" + assert settings.description == "keep me" # not overwritten + assert shared == {} # no parameters in output + + +# Patch targets live on the submitter module, since it imports these names directly. +_MOD = "vred_submitter.vred_submitter" + + +class TestPreGuiHookConfirmCallback: + """The ``settings.auto_accept`` branch in ``_pre_gui_hook_confirm_callback``.""" + + @patch(f"{_MOD}.get_setting", return_value="true") + def test_none_when_auto_accept_enabled(self, mock_get_setting): + """With settings.auto_accept enabled, hooks run without a confirmation prompt.""" + assert _pre_gui_hook_confirm_callback(parent=None) is None + mock_get_setting.assert_called_once_with("settings.auto_accept") + + @patch("qtpy.QtWidgets.QMessageBox") + @patch(f"{_MOD}.get_setting", return_value="false") + def test_dialog_fires_when_auto_accept_disabled(self, mock_get_setting, mock_msgbox): + """With settings.auto_accept disabled, invoking the returned callback actually shows the + confirmation dialog (QMessageBox.question), parented to the passed-in window. + + This exercises the real ``qt_hook_confirmation`` callback rather than mocking it out, so it + verifies the prompt fires — not merely that a non-None callback was selected. + ``run_pre_gui_hooks`` invokes ``confirm_callback(sources)`` with the hook sources; an empty + list is enough to reach the dialog. The user's answer maps from the QMessageBox reply. + """ + mock_msgbox.question.return_value = mock_msgbox.Yes + + callback = _pre_gui_hook_confirm_callback(parent="mainwin") + assert callback is not None + + result = callback([]) # no hook sources needed to reach the dialog + + assert mock_msgbox.question.call_count == 1 + # The dialog is parented to the window passed into the submitter. + assert mock_msgbox.question.call_args[0][0] == "mainwin" + # "Yes" reply → proceed. + assert result is True + + +@patch(f"{_MOD}.SubmitJobToDeadlineDialog") +@patch(f"{_MOD}.get_dpi_scale_factor", return_value=1.0) +@patch(f"{_MOD}.get_major_version", return_value="2024") +@patch(f"{_MOD}.os.getenv", return_value=None) +@patch(f"{_MOD}.apply_pre_gui_output") +@patch(f"{_MOD}.run_pre_gui_hooks", return_value={}) +@patch(f"{_MOD}._pre_gui_hook_confirm_callback") +class TestCreateSubmitterDialogPreGuiWiring: + """Unit coverage for the pre-GUI wiring in ``_create_submitter_dialog``. + + deadline-cloud is mocked (as in conftest), so these tests pin the DCC-owned wiring — the + ``PreGuiHookContext`` build, the confirm-callback selection delegated to + ``_pre_gui_hook_confirm_callback``, and applying the merged output — that + ``TestApplyPreGuiOutputForVred`` / ``TestPreGuiHookConfirmCallback`` and the integration suite + don't cover. + """ + + @staticmethod + def _submitter(): + with patch(f"{_MOD}.get_yaml_contents", return_value={"steps": []}): + return VREDSubmitter(Mock()) + + def test_hooks_run_and_output_applied_before_dialog( + self, + mock_confirm_cb, + mock_run_hooks, + mock_apply, + mock_getenv, + mock_version, + mock_dpi, + mock_dialog, + ): + """run_pre_gui_hooks is invoked with a VRED PreGuiHookContext and the confirm callback from + _pre_gui_hook_confirm_callback; its output is applied onto the settings + shared values via + apply_pre_gui_output before the dialog is built.""" + mock_run_hooks.return_value = {"name": "FromHook"} + settings = _settings() + + submitter = self._submitter() + submitter._create_submitter_dialog(settings, (Mock(), Mock())) + + mock_run_hooks.assert_called_once() + context = mock_run_hooks.call_args.args[0] + assert context.bundle_dir is None # VRED has no on-disk bundle at pre-GUI time + assert context.submitter_name == "vred" + assert context.job_name == settings.name + assert context.parameters["CondaPackages"] == "vredcore=2024*" + + # The confirm callback is delegated to the helper, parented to the submitter's window. + mock_confirm_cb.assert_called_once_with(submitter.parent_window) + assert mock_run_hooks.call_args.kwargs["confirm_callback"] is mock_confirm_cb.return_value + + # The merged output is applied onto the same settings + shared-values the dialog receives. + mock_apply.assert_called_once() + applied_output, applied_settings, applied_shared = mock_apply.call_args.args + assert applied_output == {"name": "FromHook"} + assert applied_settings is settings + seeded_shared = mock_dialog.call_args.kwargs["initial_shared_parameter_values"] + assert applied_shared is seeded_shared