diff --git a/pyproject.toml b/pyproject.toml index 66c4096..b36a4c7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -122,4 +122,4 @@ source = [ [tool.coverage.report] show_missing = true -fail_under = 61 +fail_under = 75 diff --git a/src/deadline/nuke_submitter/deadline_submitter_for_nuke.py b/src/deadline/nuke_submitter/deadline_submitter_for_nuke.py index ec845c5..b9dfe1a 100644 --- a/src/deadline/nuke_submitter/deadline_submitter_for_nuke.py +++ b/src/deadline/nuke_submitter/deadline_submitter_for_nuke.py @@ -71,7 +71,7 @@ def _get_job_template(settings: RenderSubmitterUISettings) -> dict[str, Any]: # Read DEVELOPMENT.md for instructions to create the wheels directory. wheels_path = Path(__file__).parent.parent.parent.parent / "wheels" - if not wheels_path.exists() and wheels_path.is_dir(): + if not wheels_path.is_dir(): raise RuntimeError( "The Developer Option 'Include Adaptor Wheels' is enabled, but the wheels directory does not exist:\n" + str(wheels_path) diff --git a/test/deadline_adaptor_for_nuke/unit/NukeAdaptor/test_adaptor.py b/test/deadline_adaptor_for_nuke/unit/NukeAdaptor/test_adaptor.py index 576bcec..c201434 100644 --- a/test/deadline_adaptor_for_nuke/unit/NukeAdaptor/test_adaptor.py +++ b/test/deadline_adaptor_for_nuke/unit/NukeAdaptor/test_adaptor.py @@ -454,6 +454,29 @@ def test_on_cleanup_server_not_graceful_shutdown( mock_logger.error.assert_called_once_with("Failed to shutdown the Nuke Adaptor server.") mock_server_thread.join.assert_called_once_with(timeout=0.01) + @patch("time.sleep") + @patch("deadline.nuke_adaptor.NukeAdaptor.adaptor._logger") + def test_on_cleanup_server_thread_shutdown( + self, mock_logger: Mock, mock_sleep: Mock, init_data: dict + ) -> None: + """Tests that on_cleanup reports when the server does not shutdown""" + # GIVEN + adaptor = NukeAdaptor(init_data) + + with patch( + "deadline.nuke_adaptor.NukeAdaptor.adaptor.NukeAdaptor._nuke_is_running", + new_callable=lambda: False, + ), patch.object(adaptor, "_SERVER_END_TIMEOUT_SECONDS", 0.01), patch.object( + adaptor, "_server_thread" + ) as mock_server_thread: + mock_server_thread.is_alive.side_effect = [True, False] + # WHEN + adaptor.on_cleanup() + + # THEN + mock_logger.error.assert_not_called() + mock_server_thread.join.assert_called_once_with(timeout=0.01) + @patch("time.sleep") @patch("deadline.nuke_adaptor.NukeAdaptor.adaptor.ActionsQueue.__len__", return_value=0) @patch("deadline.nuke_adaptor.NukeAdaptor.adaptor.LoggingSubprocess") @@ -573,6 +596,40 @@ def test_handle_progress( ("Eddy[ERROR] - Something terrible happened", 3), ] + @pytest.mark.parametrize("regex_index, stdout, expected_progress", handle_progess_params) + @patch("deadline.nuke_adaptor.NukeAdaptor.adaptor.NukeAdaptor.update_status") + @patch.object(NukeAdaptor, "_is_rendering", new_callable=PropertyMock(return_value=False)) + def test_handle_progress_not_rendering( + self, + mock_is_rendering: Mock, + mock_update_status: Mock, + regex_index: tuple[int, int], + stdout: tuple[str, str], + expected_progress: tuple[float, float], + init_data: dict, + ) -> None: + # GIVEN + adaptor = NukeAdaptor(init_data) + + regex_callbacks = adaptor.regex_callbacks + progress_index, output_complete_index = regex_index + output_complete_regex = regex_callbacks[output_complete_index].regex_list[0] + + # WHEN + if output_complete_match := output_complete_regex.search(stdout[1]): + adaptor._handle_output_complete(output_complete_match) + + # THEN + assert output_complete_match is not None + mock_update_status.assert_not_called() + + handle_error_params = [ + ("ERROR: Something terrible happened", 0), + ("Error: Something terrible happened", 1), + ("Error : Something terrible happened", 2), + ("Eddy[ERROR] - Something terrible happened", 3), + ] + @pytest.mark.parametrize("continue_on_error", [True, False]) @pytest.mark.parametrize("stdout, regex_index", handle_error_params) @patch("deadline.nuke_adaptor.NukeAdaptor.adaptor.NukeAdaptor.update_status") @@ -671,3 +728,34 @@ def test_does_nothing_if_nuke_not_running( # THEN assert "CANCEL REQUESTED" in caplog.text assert "Nothing to cancel because Nuke is not running" in caplog.text + + +class TestNukeAdaptor_get_major_minor_version: + """Tests for static method _get_major_minor_version""" + + @patch("deadline.nuke_adaptor.NukeAdaptor.adaptor._logger") + def test_whole_version(self, mock_logger, init_data): + adaptor = NukeAdaptor(init_data) + + # THEN + assert "13.2" == adaptor._get_major_minor_version("13.2v4") + mock_logger.info.assert_called_once_with("Using 13.2 to find Nuke executable") + + @patch("deadline.nuke_adaptor.NukeAdaptor.adaptor._logger") + def test_major_minor(self, mock_logger, init_data): + adaptor = NukeAdaptor(init_data) + + # THEN + assert "13.2" == adaptor._get_major_minor_version("13.2") + mock_logger.info.assert_called_once_with("Using 13.2 to find Nuke executable") + + @patch("deadline.nuke_adaptor.NukeAdaptor.adaptor._logger") + def test_no_major_minor_version(self, mock_logger, init_data): + adaptor = NukeAdaptor(init_data) + + # THEN + assert "13" == adaptor._get_major_minor_version("13") + mock_logger.warning.assert_called_once_with( + "Could not find major.minor information from '13'," + " using '13' to find the Nuke executable" + ) diff --git a/test/deadline_adaptor_for_nuke/unit/NukeClient/test_nuke_handler.py b/test/deadline_adaptor_for_nuke/unit/NukeClient/test_nuke_handler.py index c0dac3b..20b6244 100644 --- a/test/deadline_adaptor_for_nuke/unit/NukeClient/test_nuke_handler.py +++ b/test/deadline_adaptor_for_nuke/unit/NukeClient/test_nuke_handler.py @@ -276,6 +276,19 @@ def test_set_write_nodes(self, write_nodes: List[MockNode], nukehandler: NukeHan # THEN assert nukehandler.write_nodes == write_nodes + def test_set_write_nodes_all_write_nodes( + self, write_nodes: List[MockNode], nukehandler: NukeHandler + ): + # GIVEN + all_write_nodes = ["All Write Nodes"] + data = {"write_nodes": all_write_nodes} + + # WHEN + nukehandler.set_write_nodes(data) + + # THEN + assert nukehandler.write_nodes == write_nodes + missing_nodes_params = [ ( SortedList(["these", "do", "not", "exist"]), diff --git a/test/deadline_submitter_for_nuke/unit/test_assets.py b/test/deadline_submitter_for_nuke/unit/test_assets.py index c519f51..9b977ea 100644 --- a/test/deadline_submitter_for_nuke/unit/test_assets.py +++ b/test/deadline_submitter_for_nuke/unit/test_assets.py @@ -8,6 +8,7 @@ import nuke import pytest +from deadline.client.exceptions import DeadlineOperationError from deadline.nuke_submitter.assets import ( find_all_write_nodes, get_node_file_knob_paths, @@ -75,6 +76,25 @@ def test_get_scene_asset_references( assert all(asset in results.input_filenames for asset in expected_assets) +@patch("os.path.isfile", return_value=False) +@patch("deadline.nuke_submitter.assets.get_nuke_script_file", return_value="/this/scriptfile.nk") +def test_get_scene_asset_references_script_not_saved( + mock_get_nuke_script_file: Mock, mock_path_isfile: Mock +): + # GIVEN + nuke.allNodes.return_value = [] + + # WHEN + with pytest.raises(DeadlineOperationError) as exc_info: + get_scene_asset_references() + + # THEN + error_msg = ( + "The Nuke Script is not saved to disk. Please save it before opening the submitter dialog." + ) + assert str(exc_info.value) == error_msg + + def test_find_all_write_nodes(): # GIVEN nuke.allNodes.return_value = [] diff --git a/test/deadline_submitter_for_nuke/unit/test_deadline_submitter_for_nuke.py b/test/deadline_submitter_for_nuke/unit/test_deadline_submitter_for_nuke.py new file mode 100644 index 0000000..1f0af32 --- /dev/null +++ b/test/deadline_submitter_for_nuke/unit/test_deadline_submitter_for_nuke.py @@ -0,0 +1,489 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + +from __future__ import annotations + +from typing import List + +import pytest + +import dataclasses +import json +from json import JSONDecodeError +from pathlib import Path +from unittest.mock import MagicMock, patch, mock_open + +import nuke + +from deadline.nuke_submitter.deadline_submitter_for_nuke import ( + _get_sticky_settings_file, + _load_sticky_settings, + _save_sticky_settings, + get_nuke_version, + _get_write_node, + _get_job_template, + _get_parameter_values, +) +from deadline.nuke_submitter.data_classes.submission import ( + RenderSubmitterSettings, + RenderSubmitterUISettings, +) +from .test_template_output.expected_job_template_output import EXPECTED_NUKE_JOB_TEMPLATE +from .test_template_output.expected_job_template_with_wheel_output import ( + EXPECTED_NUKE_JOB_TEMPLATE_WITH_WHEEL, +) + +TEST_NUKE_VERSION = "13.2v1" +TEST_NUKE_SCRIPT_FILE_PATH = "/some/path.nk" +REZ_PACKAGE_DEFAULT = "nuke-13 deadline_cloud_for_nuke" + + +@pytest.fixture +def mock_nuke_file_path(): + with patch( + "deadline.nuke_submitter.deadline_submitter_for_nuke.get_nuke_script_file", + return_value=TEST_NUKE_SCRIPT_FILE_PATH, + ) as m: + yield m + + +@pytest.fixture +def mock_sticky_file_path(): + with patch( + "deadline.nuke_submitter.deadline_submitter_for_nuke._get_sticky_settings_file", + return_value=Path("/some/path.deadline_settings.json"), + ) as m: + yield m + + +@pytest.fixture +def mock_is_file(): + with patch("pathlib.Path.is_file", return_value=True) as m: + yield m + + +@pytest.fixture +def mock_is_dir(): + with patch("pathlib.Path.is_dir", return_value=True) as m: + yield m + + +@pytest.fixture +def mock_nuke_version(): + with patch( + "deadline.nuke_submitter.deadline_submitter_for_nuke.get_nuke_version", + return_value=TEST_NUKE_VERSION, + ) as m: + yield m + + +def _get_customized_settings() -> RenderSubmitterSettings: + return RenderSubmitterSettings( + name="TestName", + description="TestDescription", + override_rez_packages=False, + rez_packages="", + override_frame_range=True, + frame_list="1-10:2", + write_node_selection="TestWriteNode", + view_selection="TestView", + is_proxy_mode=True, + initial_status="PAUSED", + max_failed_tasks_count=0, + max_retries_per_task=0, + priority=51, + input_directories=["/path/to/input"], + output_directories=["/path/to/output"], + input_filenames=["myrender.exr"], + include_adaptor_wheels=False, + ) + + +@pytest.fixture +def base_parameters() -> List[dict]: + return [ + {"name": "deadline:priority", "value": 51}, + {"name": "deadline:targetTaskRunStatus", "value": "PAUSED"}, + {"name": "deadline:maxFailedTasksCount", "value": 0}, + {"name": "deadline:maxRetriesPerTask", "value": 0}, + ] + + +@pytest.fixture +def customized_settings() -> RenderSubmitterSettings: + return _get_customized_settings() + + +@pytest.fixture() +def mock_write_node(): + mock_write_node = MagicMock() + mock_write_node.fullName.return_value = "TestWriteNode" + mock_write_node.frameRange.return_value = "1-5" + return mock_write_node + + +@pytest.fixture +def customized_ui_settings(mock_write_node) -> RenderSubmitterUISettings: + settings = _get_customized_settings() + ui_settings = RenderSubmitterUISettings(name="TestName") + ui_settings.apply_saved_settings(settings) + ui_settings.write_node_selection = mock_write_node + return ui_settings + + +@pytest.fixture +def customized_ui_settings_no_write_node() -> RenderSubmitterUISettings: + settings = _get_customized_settings() + ui_settings = RenderSubmitterUISettings(name="TestName") + ui_settings.apply_saved_settings(settings) + ui_settings.write_node_selection = None + return ui_settings + + +@pytest.fixture +def customized_ui_settings_root() -> RenderSubmitterUISettings: + settings = _get_customized_settings() + ui_settings = RenderSubmitterUISettings(name="TestName") + ui_settings.apply_saved_settings(settings) + ui_settings.write_node_selection = nuke.root() + return ui_settings + + +@pytest.fixture +def mock_read_data(customized_settings) -> str: + return json.dumps(dataclasses.asdict(customized_settings)) + + +def test_get_sticky_settings_file(mock_nuke_file_path, mock_is_file): + # This is a dummy test that just adds coverage, there are probably better ways to mock a file or create a test .nk + # GIVEN + expected_path = Path("/some/path.deadline_settings.json") + + # WHEN + result = _get_sticky_settings_file() + + # THEN + assert result == expected_path + + +@patch("deadline.nuke_submitter.deadline_submitter_for_nuke.get_nuke_script_file", return_value="") +def test_get_sticky_settings_file_no_script(mock_no_nuke_script_file): + # THEN + assert _get_sticky_settings_file() is None + + +@patch("pathlib.Path.is_file", return_value=False) +def test_get_sticky_settings_file_not_file(mock_path_is_not_file, mock_nuke_file_path): + # THEN + assert _get_sticky_settings_file() is None + + +def test_load_sticky_settings(mock_sticky_file_path, mock_is_file, customized_settings): + # GIVEN + read_data = json.dumps(dataclasses.asdict(customized_settings)) + + # WHEN + with patch("builtins.open", mock_open(read_data=read_data)): + loaded_settings = _load_sticky_settings() + + # THEN + assert loaded_settings is not None + assert loaded_settings == customized_settings + + +@patch( + "deadline.nuke_submitter.deadline_submitter_for_nuke._get_sticky_settings_file", + return_value=None, +) +def test_load_sticky_settings_no_setting_file(mock_no_sticky_setting): + # THEN + assert _load_sticky_settings() is None + + +@patch("pathlib.Path.is_file", return_value=False) +def test_load_sticky_settings_not_file(mock_is_not_file, mock_sticky_file_path): + # THEN + assert _load_sticky_settings() is None + + +@patch("builtins.open", new_callable=mock_open) +def test_load_sticky_settings_os_error(mock_file, mock_sticky_file_path, mock_is_file): + # GIVEN + mock_file.side_effect = OSError() + + # THEN + with pytest.raises(RuntimeError) as exc_info: + _load_sticky_settings() + + assert str(exc_info.value) == "Failed to read from settings file" + + +@patch("builtins.open", new_callable=mock_open) +def test_load_sticky_settings_json_decode_error(mock_file, mock_sticky_file_path, mock_is_file): + # GIVEN + mock_file.side_effect = JSONDecodeError(msg="msg", doc="doc", pos=0) + + # THEN + with pytest.raises(RuntimeError) as exc_info: + _load_sticky_settings() + + assert str(exc_info.value) == "Failed to parse JSON from settings file" + + +@patch("builtins.open", new_callable=mock_open) +def test_load_sticky_settings_json_type_error(mock_file, mock_sticky_file_path, mock_is_file): + # GIVEN + mock_file.side_effect = TypeError() + + # THEN + with pytest.raises(RuntimeError) as exc_info: + _load_sticky_settings() + + assert str(exc_info.value) == "Failed to deserialize settings data" + + +def test_save_sticky_settings(mock_sticky_file_path, customized_ui_settings): + # GIVEN + expected_write_data = json.dumps( + dataclasses.asdict(customized_ui_settings.to_render_submitter_settings()), indent=2 + ) + with patch("builtins.open", mock_open()) as mock_file: + _save_sticky_settings(customized_ui_settings) + mock_file.assert_called_with( + Path("/some/path.deadline_settings.json"), "w", encoding="utf8" + ) + handle = mock_file() + handle.write.assert_called_with(expected_write_data) + + +@patch( + "deadline.nuke_submitter.deadline_submitter_for_nuke._get_sticky_settings_file", + return_value=None, +) +def test_save_sticky_settings_no_setting_file(mock_no_sticky_setting, customized_ui_settings): + # THEN + _save_sticky_settings(customized_ui_settings) + mock_no_sticky_setting.assert_called_once() + + +@patch("builtins.open", new_callable=mock_open) +def test_save_sticky_settings_os_error(mock_file, mock_sticky_file_path, customized_ui_settings): + # GIVEN + mock_file.side_effect = OSError() + + # THEN + with pytest.raises(RuntimeError) as exc_info: + _save_sticky_settings(customized_ui_settings) + + assert str(exc_info.value) == "Failed to write to settings file" + + +def test_get_nuke_version(): + assert nuke.env["NukeVersionString"] == get_nuke_version() + + +def test_get_write_node_node_is_not_root(customized_ui_settings): + result = _get_write_node(customized_ui_settings) + assert "TestWriteNode" == result[1] + + +def test_get_write_node_node_is_root(customized_ui_settings_root): + # WHEN + result = _get_write_node(customized_ui_settings_root) + + # THEN + assert "" == result[1] + assert nuke.root() == result[0] + + +def test_get_write_node_node_is_none(customized_ui_settings_no_write_node): + # WHEN + result = _get_write_node(customized_ui_settings_no_write_node) + + # THEN + assert "" == result[1] + assert nuke.root() == result[0] + + +@patch( + "deadline.nuke_submitter.deadline_submitter_for_nuke.find_all_write_nodes", return_value=set() +) +def test_get_job_template(mock_fall_all_node, mock_is_dir, customized_ui_settings): + assert EXPECTED_NUKE_JOB_TEMPLATE == _get_job_template(customized_ui_settings) + + +@patch( + "os.listdir", + return_value=["openjd-wheel.whl", "deadline-wheel.whl", "deadline_cloud_for_nuke-wheel.whl"], +) +@patch( + "deadline.nuke_submitter.deadline_submitter_for_nuke.find_all_write_nodes", return_value=set() +) +def test_get_job_template_with_wheel( + mock_fall_all_node, mock_listdir, mock_is_dir, customized_ui_settings +): + # GIVEN + customized_ui_settings.include_adaptor_wheels = True + + # WHEN + result = _get_job_template(customized_ui_settings) + + # override the default wheel directory filepath from the result for comparison since each os/workstation will + # generate different path, and regex is too expensive for this comparison. + result["parameterDefinitions"] + for param in result["parameterDefinitions"]: + if param["name"] == "AdaptorWheels": + param["default"] = "/test/directory/deadline-cloud-for-nuke/wheels" + + # THEN + assert EXPECTED_NUKE_JOB_TEMPLATE_WITH_WHEEL == result + + +def test_get_job_template_with_no_wheel_directories(customized_ui_settings): + expected_info = "The Developer Option 'Include Adaptor Wheels' is enabled, but the wheels directory does not exist:" + customized_ui_settings.include_adaptor_wheels = True + + # WHEN + with pytest.raises(RuntimeError) as exc_info: + _get_job_template(customized_ui_settings) + + # THEN + assert expected_info in str(exc_info.value) + + +@patch("os.listdir", return_value=["openjd-wheel.whl"]) +def test_get_job_template_with_missing_wheel_directories( + mock_listdir, mock_is_dir, customized_ui_settings +): + customized_ui_settings.include_adaptor_wheels = True + + # WHEN + with pytest.raises(RuntimeError) as exc_info: + _get_job_template(customized_ui_settings) + + # THEN + assert str(exc_info.value) == ( + "The Developer Option 'Include Adaptor Wheels' is enabled, " + "but the wheels directory contains the wrong wheels:\n" + "Expected: openjd, deadline, and deadline_cloud_for_nuke\n" + "Actual: {'openjd'}" + ) + + +def test_get_parameter_values( + base_parameters, mock_nuke_file_path, mock_nuke_version, customized_ui_settings +): + # GIVEN + expected_parameter_values = base_parameters + expected_parameter_values.append({"name": "Frames", "value": "1-10:2"}) + expected_parameter_values.append( + {"name": "NukeScriptFile", "value": TEST_NUKE_SCRIPT_FILE_PATH} + ) + expected_parameter_values.append({"name": "WriteNode", "value": "TestWriteNode"}) + expected_parameter_values.append({"name": "View", "value": "TestView"}) + expected_parameter_values.append({"name": "ProxyMode", "value": "true"}) + expected_parameter_values.append({"name": "NukeVersion", "value": TEST_NUKE_VERSION}) + + expected_parameter_dict = {"parameterValues": expected_parameter_values} + + # WHEN + result = _get_parameter_values(customized_ui_settings) + + # THEN + assert expected_parameter_dict == result + + +def test_get_parameter_values_override_frame_range_false( + base_parameters, mock_nuke_file_path, mock_nuke_version, customized_ui_settings +): + expected_parameter_values = base_parameters + expected_parameter_values.append({"name": "Frames", "value": "1-5"}) + expected_parameter_values.append( + {"name": "NukeScriptFile", "value": TEST_NUKE_SCRIPT_FILE_PATH} + ) + expected_parameter_values.append({"name": "WriteNode", "value": "TestWriteNode"}) + expected_parameter_values.append({"name": "View", "value": "TestView"}) + expected_parameter_values.append({"name": "ProxyMode", "value": "true"}) + expected_parameter_values.append({"name": "NukeVersion", "value": TEST_NUKE_VERSION}) + + expected_parameter_dict = {"parameterValues": expected_parameter_values} + + settings = customized_ui_settings + settings.override_frame_range = False + + # WHEN + result = _get_parameter_values(settings) + + # THEN + assert expected_parameter_dict == result + + +def test_get_parameter_values_no_write_node_name( + base_parameters, mock_nuke_file_path, mock_nuke_version, customized_ui_settings_no_write_node +): + expected_parameter_values = base_parameters + expected_parameter_values.append({"name": "Frames", "value": "1-10:2"}) + expected_parameter_values.append( + {"name": "NukeScriptFile", "value": TEST_NUKE_SCRIPT_FILE_PATH} + ) + expected_parameter_values.append({"name": "View", "value": "TestView"}) + expected_parameter_values.append({"name": "ProxyMode", "value": "true"}) + expected_parameter_values.append({"name": "NukeVersion", "value": TEST_NUKE_VERSION}) + + expected_parameter_dict = {"parameterValues": expected_parameter_values} + + # WHEN + result = _get_parameter_values(customized_ui_settings_no_write_node) + + # THEN + assert expected_parameter_dict == result + + +def test_get_parameter_values_no_view_selection( + base_parameters, mock_nuke_file_path, mock_nuke_version, customized_ui_settings +): + expected_parameter_values = base_parameters + expected_parameter_values.append({"name": "Frames", "value": "1-10:2"}) + expected_parameter_values.append( + {"name": "NukeScriptFile", "value": TEST_NUKE_SCRIPT_FILE_PATH} + ) + expected_parameter_values.append({"name": "WriteNode", "value": "TestWriteNode"}) + expected_parameter_values.append({"name": "ProxyMode", "value": "true"}) + expected_parameter_values.append({"name": "NukeVersion", "value": TEST_NUKE_VERSION}) + + expected_parameter_dict = {"parameterValues": expected_parameter_values} + + settings = customized_ui_settings + settings.view_selection = None + + # WHEN + result = _get_parameter_values(settings) + + # THEN + assert expected_parameter_dict == result + + +def test_get_parameter_values_override_rez_packages( + base_parameters, mock_nuke_file_path, mock_nuke_version, customized_ui_settings +): + expected_parameter_values = base_parameters + expected_parameter_values.append({"name": "Frames", "value": "1-10:2"}) + expected_parameter_values.append( + {"name": "NukeScriptFile", "value": TEST_NUKE_SCRIPT_FILE_PATH} + ) + expected_parameter_values.append({"name": "WriteNode", "value": "TestWriteNode"}) + expected_parameter_values.append({"name": "View", "value": "TestView"}) + expected_parameter_values.append({"name": "ProxyMode", "value": "true"}) + expected_parameter_values.append({"name": "NukeVersion", "value": TEST_NUKE_VERSION}) + expected_parameter_values.append({"name": "RezPackages", "value": REZ_PACKAGE_DEFAULT}) + + expected_parameter_dict = {"parameterValues": expected_parameter_values} + + settings = customized_ui_settings + settings.override_rez_packages = True + settings.rez_packages = REZ_PACKAGE_DEFAULT + + # WHEN + result = _get_parameter_values(settings) + + # THEN + assert expected_parameter_dict == result diff --git a/test/deadline_submitter_for_nuke/unit/test_job_bundle_output_test_runner.py b/test/deadline_submitter_for_nuke/unit/test_job_bundle_output_test_runner.py new file mode 100644 index 0000000..56c5cdc --- /dev/null +++ b/test/deadline_submitter_for_nuke/unit/test_job_bundle_output_test_runner.py @@ -0,0 +1,36 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + +from __future__ import annotations + +from unittest.mock import patch + +from deadline.nuke_submitter.job_bundle_output_test_runner import ( + _get_dcc_scene_file_extension, + _open_dcc_scene_file, + _close_dcc_scene_file, +) + + +def test_get_dcc_main_window(): + assert ".nk" == _get_dcc_scene_file_extension() + + +@patch("nuke.scriptOpen") +def test_open_dcc_scene_file(mock_script_open): + # GIVEN + test_file_name = "sample.nk" + + # WHEN + _open_dcc_scene_file(test_file_name) + + # THEN + mock_script_open.assert_called_once_with(test_file_name) + + +@patch("nuke.scriptClose") +def test_close_dcc_scene_file(mock_script_close): + # WHEN + _close_dcc_scene_file() + + # THEN + mock_script_close.assert_called_once() diff --git a/test/deadline_submitter_for_nuke/unit/test_template_output/expected_job_template_output.py b/test/deadline_submitter_for_nuke/unit/test_template_output/expected_job_template_output.py new file mode 100644 index 0000000..df7ba74 --- /dev/null +++ b/test/deadline_submitter_for_nuke/unit/test_template_output/expected_job_template_output.py @@ -0,0 +1,192 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + +EXPECTED_NUKE_JOB_TEMPLATE = { + "specificationVersion": "jobtemplate-2023-09", + "name": "TestName", + "parameterDefinitions": [ + { + "name": "NukeScriptFile", + "type": "PATH", + "objectType": "FILE", + "dataFlow": "IN", + "userInterface": { + "control": "CHOOSE_INPUT_FILE", + "label": "Nuke Script File", + "fileFilters": [ + {"label": "Nuke Script Files", "patterns": ["*.nk"]}, + {"label": "All Files", "patterns": ["*"]}, + ], + }, + "description": "The Nuke script file to render.", + }, + { + "name": "Frames", + "type": "STRING", + "description": "The frames to render. E.g. 1-3,8,11-15", + "minLength": 1, + }, + { + "name": "WriteNode", + "type": "STRING", + "userInterface": {"control": "DROPDOWN_LIST", "label": "Write Node"}, + "description": "Which write node to render ('All Write Nodes' for all of them)", + "default": "All Write Nodes", + "allowedValues": ["All Write Nodes"], + }, + { + "name": "View", + "type": "STRING", + "userInterface": {"control": "DROPDOWN_LIST"}, + "description": "Which view to render ('All Views' for all of them)", + "default": "All Views", + "allowedValues": ["All Views"], + }, + { + "name": "ProxyMode", + "type": "STRING", + "userInterface": {"control": "CHECK_BOX", "label": "Proxy Mode"}, + "description": "Render in Proxy Mode.", + "default": "false", + "allowedValues": ["true", "false"], + }, + { + "name": "ContinueOnError", + "type": "STRING", + "userInterface": {"control": "CHECK_BOX", "label": "Continue On Error"}, + "description": "Continue processing when errors occur.", + "default": "false", + "allowedValues": ["true", "false"], + }, + { + "name": "NukeVersion", + "type": "STRING", + "userInterface": {"control": "LINE_EDIT", "label": "Nuke Version"}, + "description": "The version of Nuke.", + }, + { + "name": "RezPackages", + "type": "STRING", + "userInterface": {"control": "LINE_EDIT", "label": "Rez Packages"}, + "description": "A space-separated list of Rez packages to install", + "default": "nuke-13 deadline_cloud_for_nuke", + }, + ], + "jobEnvironments": [ + { + "name": "Rez", + "description": "Initializes and destroys the Rez environment for the job.", + "script": { + "actions": { + "onEnter": {"command": "{{ Env.File.Enter }}"}, + "onExit": {"command": "{{ Env.File.Exit }}"}, + }, + "embeddedFiles": [ + { + "name": "Enter", + "filename": "rez-enter.sh", + "type": "TEXT", + "runnable": True, + "data": '#!/bin/env bash\n\nset -euo pipefail\n\nif [ ! -z "{{Param.RezPackages}}" ]; then\n echo "Rez Package List:"\n echo " {{Param.RezPackages}}"\n\n # Create the environment\n /usr/local/bin/deadline-rez init \\\n -d "{{Session.WorkingDirectory}}" \\\n {{Param.RezPackages}}\n\n # Capture the environment\'s vars\n {{Env.File.InitialVars}}\n . /usr/local/bin/deadline-rez activate \\\n -d "{{Session.WorkingDirectory}}"\n {{Env.File.CaptureVars}}\nelse\n echo "No Rez Packages, skipping environment creation."\nfi\n', + }, + { + "name": "Exit", + "filename": "rez-exit.sh", + "type": "TEXT", + "runnable": True, + "data": '#!/bin/env bash\n\nset -euo pipefail\n\nif [ ! -z "{{Param.RezPackages}}" ]; then\n echo "Rez Package List:"\n echo " {{Param.RezPackages}}"\n\n /usr/local/bin/deadline-rez destroy \\\n -d "{{ Session.WorkingDirectory }}"\nelse\n echo "No Rez Packages, skipping environment teardown."\nfi\n', + }, + { + "name": "InitialVars", + "filename": "initial-vars.sh", + "type": "TEXT", + "runnable": True, + "data": '#!/usr/bin/env python3\nimport os, json\nenvfile = "{{Session.WorkingDirectory}}/.envInitial"\nwith open(envfile, "w", encoding="utf8") as f:\n json.dump(dict(os.environ), f)\n', + }, + { + "name": "CaptureVars", + "filename": "capture-vars.sh", + "type": "TEXT", + "runnable": True, + "data": '#!/usr/bin/env python3\nimport os, json, sys\nenvfile = "{{Session.WorkingDirectory}}/.envInitial"\nif os.path.isfile(envfile):\n with open(envfile, "r", encoding="utf8") as f:\n before = json.load(f)\nelse:\n print("No initial environment found, must run Env.File.CaptureVars script first")\n sys.exit(1)\nafter = dict(os.environ)\n\nput = {k: v for k, v in after.items() if v != before.get(k)}\ndelete = {k for k in before if k not in after}\n\nfor k, v in put.items():\n print(f"updating {k}={v}")\n print(f"openjd_env: {k}={v}")\nfor k in delete:\n print(f"openjd_unset_env: {k}")\n', + }, + ], + }, + } + ], + "steps": [ + { + "name": "Render", + "parameterSpace": { + "taskParameterDefinitions": [ + {"name": "Frame", "type": "INT", "range": "{{Param.Frames}}"} + ] + }, + "stepEnvironments": [ + { + "name": "Nuke", + "description": "Runs Nuke in the background with a script file loaded.", + "script": { + "embeddedFiles": [ + { + "name": "initData", + "filename": "init-data.yaml", + "type": "TEXT", + "data": "continue_on_error: {{Param.ContinueOnError}}\nproxy: {{Param.ProxyMode}}\nscript_file: '{{Param.NukeScriptFile}}'\nversion: '{{Param.NukeVersion}}'\nwrite_nodes:\n- '{{Param.WriteNode}}'\nviews:\n- '{{Param.View}}'\n", + } + ], + "actions": { + "onEnter": { + "command": "NukeAdaptor", + "args": [ + "daemon", + "start", + "--path-mapping-rules", + "file://{{Session.PathMappingRulesFile}}", + "--connection-file", + "{{Session.WorkingDirectory}}/connection.json", + "--init-data", + "file://{{ Env.File.initData }}", + ], + "cancelation": {"mode": "NOTIFY_THEN_TERMINATE"}, + }, + "onExit": { + "command": "NukeAdaptor", + "args": [ + "daemon", + "stop", + "--connection-file", + "{{ Session.WorkingDirectory }}/connection.json", + ], + "cancelation": {"mode": "NOTIFY_THEN_TERMINATE"}, + }, + }, + }, + } + ], + "script": { + "embeddedFiles": [ + { + "name": "runData", + "filename": "run-data.yaml", + "type": "TEXT", + "data": "frame: {{Task.Param.Frame}}", + } + ], + "actions": { + "onRun": { + "command": "NukeAdaptor", + "args": [ + "daemon", + "run", + "--connection-file", + "{{Session.WorkingDirectory}}/connection.json", + "--run-data", + "file://{{ Task.File.runData }}", + ], + "cancelation": {"mode": "NOTIFY_THEN_TERMINATE"}, + } + }, + }, + } + ], +} diff --git a/test/deadline_submitter_for_nuke/unit/test_template_output/expected_job_template_with_wheel_output.py b/test/deadline_submitter_for_nuke/unit/test_template_output/expected_job_template_with_wheel_output.py new file mode 100644 index 0000000..d035a86 --- /dev/null +++ b/test/deadline_submitter_for_nuke/unit/test_template_output/expected_job_template_with_wheel_output.py @@ -0,0 +1,196 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + +EXPECTED_NUKE_JOB_TEMPLATE_WITH_WHEEL = { + "specificationVersion": "jobtemplate-2023-09", + "name": "TestName", + "parameterDefinitions": [ + { + "name": "NukeScriptFile", + "type": "PATH", + "objectType": "FILE", + "dataFlow": "IN", + "userInterface": { + "control": "CHOOSE_INPUT_FILE", + "label": "Nuke Script File", + "fileFilters": [ + {"label": "Nuke Script Files", "patterns": ["*.nk"]}, + {"label": "All Files", "patterns": ["*"]}, + ], + }, + "description": "The Nuke script file to render.", + }, + { + "name": "Frames", + "type": "STRING", + "description": "The frames to render. E.g. 1-3,8,11-15", + "minLength": 1, + }, + { + "name": "WriteNode", + "type": "STRING", + "userInterface": {"control": "DROPDOWN_LIST", "label": "Write Node"}, + "description": "Which write node to render ('All Write Nodes' for all of them)", + "default": "All Write Nodes", + "allowedValues": ["All Write Nodes"], + }, + { + "name": "View", + "type": "STRING", + "userInterface": {"control": "DROPDOWN_LIST"}, + "description": "Which view to render ('All Views' for all of them)", + "default": "All Views", + "allowedValues": ["All Views"], + }, + { + "name": "ProxyMode", + "type": "STRING", + "userInterface": {"control": "CHECK_BOX", "label": "Proxy Mode"}, + "description": "Render in Proxy Mode.", + "default": "false", + "allowedValues": ["true", "false"], + }, + { + "name": "ContinueOnError", + "type": "STRING", + "userInterface": {"control": "CHECK_BOX", "label": "Continue On Error"}, + "description": "Continue processing when errors occur.", + "default": "false", + "allowedValues": ["true", "false"], + }, + { + "name": "NukeVersion", + "type": "STRING", + "userInterface": {"control": "LINE_EDIT", "label": "Nuke Version"}, + "description": "The version of Nuke.", + }, + { + "name": "RezPackages", + "type": "STRING", + "userInterface": {"control": "LINE_EDIT", "label": "Rez Packages"}, + "description": "A space-separated list of Rez packages to install", + "default": "nuke-13 deadline_cloud_for_nuke", + }, + { + "name": "AdaptorWheels", + "type": "PATH", + "objectType": "DIRECTORY", + "dataFlow": "IN", + "description": "A directory that contains wheels for openjd, deadline, and the overridden adaptor.", + "default": "/test/directory/deadline-cloud-for-nuke/wheels", + }, + { + "name": "OverrideAdaptorName", + "type": "STRING", + "description": "The name of the adaptor to override, for example NukeAdaptor or MayaAdaptor.", + "default": "NukeAdaptor", + }, + ], + "jobEnvironments": [ + { + "name": "OverrideAdaptor", + "description": "Replaces the default Adaptor in the environment's PATH with one from the packages in the AdaptorWheels attached directory.\n", + "script": { + "actions": {"onEnter": {"command": "{{Env.File.Enter}}"}}, + "embeddedFiles": [ + { + "name": "Enter", + "filename": "override-adaptor-enter.sh", + "type": "TEXT", + "runnable": True, + "data": '#!/bin/env bash\n\nset -euo pipefail\n\necho "The adaptor wheels that are attached to the job:"\nls {{Param.AdaptorWheels}}/\necho ""\n\n# Create a venv and activate it in this environment\necho "Creating Python venv for the {{Param.OverrideAdaptor}} command"\n/usr/local/bin/python3 -m venv \'{{Session.WorkingDirectory}}/venv\'\n{{Env.File.InitialVars}}\n. \'{{Session.WorkingDirectory}}/venv/bin/activate\'\n{{Env.File.CaptureVars}}\necho ""\n\necho "Installing adaptor into the venv"\npip install {{Param.AdaptorWheels}}/openjd*.whl\npip install {{Param.AdaptorWheels}}/deadline*.whl\necho ""\n\nif [ ! -f \'{{Session.WorkingDirectory}}/venv/bin/{{Param.OverrideAdaptorName}}\' ]; then\n echo "The Override Adaptor {{Param.OverrideAdaptorName}} was not installed as expected."\n exit 1\nfi\n', + }, + { + "name": "InitialVars", + "filename": "initial-vars", + "type": "TEXT", + "runnable": True, + "data": '#!/usr/bin/env python3\nimport os, json\nenvfile = "{{Session.WorkingDirectory}}/.envInitial"\nwith open(envfile, "w", encoding="utf8") as f:\n json.dump(dict(os.environ), f)\n', + }, + { + "name": "CaptureVars", + "filename": "capture-vars", + "type": "TEXT", + "runnable": True, + "data": '#!/usr/bin/env python3\nimport os, json, sys\nenvfile = "{{Session.WorkingDirectory}}/.envInitial"\nif os.path.isfile(envfile):\n with open(envfile, "r", encoding="utf8") as f:\n before = json.load(f)\nelse:\n print("No initial environment found, must run Env.File.CaptureVars script first")\n sys.exit(1)\nafter = dict(os.environ)\n\nput = {k: v for k, v in after.items() if v != before.get(k)}\ndelete = {k for k in before if k not in after}\n\nfor k, v in put.items():\n print(f"updating {k}={v}")\n print(f"openjd_env: {k}={v}")\nfor k in delete:\n print(f"openjd_unset_env: {k}")\n', + }, + ], + }, + } + ], + "steps": [ + { + "name": "Render", + "parameterSpace": { + "taskParameterDefinitions": [ + {"name": "Frame", "type": "INT", "range": "{{Param.Frames}}"} + ] + }, + "stepEnvironments": [ + { + "name": "Nuke", + "description": "Runs Nuke in the background with a script file loaded.", + "script": { + "embeddedFiles": [ + { + "name": "initData", + "filename": "init-data.yaml", + "type": "TEXT", + "data": "continue_on_error: {{Param.ContinueOnError}}\nproxy: {{Param.ProxyMode}}\nscript_file: '{{Param.NukeScriptFile}}'\nversion: '{{Param.NukeVersion}}'\nwrite_nodes:\n- '{{Param.WriteNode}}'\nviews:\n- '{{Param.View}}'\n", + } + ], + "actions": { + "onEnter": { + "command": "NukeAdaptor", + "args": [ + "daemon", + "start", + "--path-mapping-rules", + "file://{{Session.PathMappingRulesFile}}", + "--connection-file", + "{{Session.WorkingDirectory}}/connection.json", + "--init-data", + "file://{{ Env.File.initData }}", + ], + "cancelation": {"mode": "NOTIFY_THEN_TERMINATE"}, + }, + "onExit": { + "command": "NukeAdaptor", + "args": [ + "daemon", + "stop", + "--connection-file", + "{{ Session.WorkingDirectory }}/connection.json", + ], + "cancelation": {"mode": "NOTIFY_THEN_TERMINATE"}, + }, + }, + }, + } + ], + "script": { + "embeddedFiles": [ + { + "name": "runData", + "filename": "run-data.yaml", + "type": "TEXT", + "data": "frame: {{Task.Param.Frame}}", + } + ], + "actions": { + "onRun": { + "command": "NukeAdaptor", + "args": [ + "daemon", + "run", + "--connection-file", + "{{Session.WorkingDirectory}}/connection.json", + "--run-data", + "file://{{ Task.File.runData }}", + ], + "cancelation": {"mode": "NOTIFY_THEN_TERMINATE"}, + } + }, + }, + } + ], +}