diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..7bb8e25 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,17 @@ +{ + "python.linting.pylintEnabled": false, + "python.linting.pep8Enabled": true, + "python.linting.enabled": true, + "python.testing.unittestEnabled": true, + "python.testing.pyTestEnabled": false, + "python.testing.nosetestsEnabled": false, + "python.pythonPath": "${env:GAFFER_ROOT}/bin/python.exe", + "python.testing.cwd": "./python", + "python.testing.unittestArgs": [ + "-v", + "-s", + "./python/GafferDeadlineTest", + "-p", + "*.py" + ] +} \ No newline at end of file diff --git a/GafferDeadline.code-workspace b/GafferDeadline.code-workspace new file mode 100644 index 0000000..876a149 --- /dev/null +++ b/GafferDeadline.code-workspace @@ -0,0 +1,8 @@ +{ + "folders": [ + { + "path": "." + } + ], + "settings": {} +} \ No newline at end of file diff --git a/LICENSE.md b/LICENSE.md index e4c60c7..aa5b52f 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,5 +1,5 @@ -Copyright (c) 2019, Eric Mehl +Copyright (c) 2019, Hypothetical Inc. All rights reserved. Redistribution and use in source and binary forms, with or without @@ -12,7 +12,7 @@ modification, are permitted provided that the following conditions are met: this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. -* Neither the name of [project] nor the names of its +* Neither the name of Hypothetical Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. diff --git a/README.md b/README.md index 00423d2..cb4491a 100644 --- a/README.md +++ b/README.md @@ -1 +1,47 @@ -GafferDeadline +# GafferDeadline # +Deadline Dispatcher for Gaffer. There are three components - the Gaffer dispatcher, Deadline plugin and Python dependency script called by Deadline to check for task dependencies getting released. + +Gaffer can generate arbitrarily complex node trees with dependencies between Task Nodes that are more complicated than Deadline supports through it's standard job and task dependencies. To support Gaffer's DAG style of dependencies, a dependency script is included. Deadline runs this script periodically for each job to determine which, if any, tasks for that job are ready to be released. Using the included dispatcher and plugin this should be transparent to the user once everything is setup as described below. + +GafferDeadline will auto-detect the most efficient method of setting up dependencies by default (see not in Usage about overriding this behavior). Most Gaffer scripts will likely be submitted with frame-to-frame dependencies, but it will fall back to the scripted task dependency system or job-to-job on a per-node basis if it doesn't fit within Deadline's dependency scheme. + +It is tested on Linux and the beta Windows Gaffer build. OS X compatibility is unknown. + +## Installing ## + 1. Extract the archive / clone the repostory to a directory accessible to Gaffer. + 2. Move the "Gaffer" subdirectory to your Deadline repository "custom/plugins" directory. This is the Deadline plugin that will run Gaffer jobs on your render farm. + 3. Add the directory where you extracted / cloned the repository to the GAFFER_EXTENSION_PATHS environment variable before running Gaffer. + 4. Move the gaffer_batch_dependency.py file to a location where all of your Deadline Slaves and Pulse machines can access the file. Deadline will run that script according to your repository settings to check for tasks that can be released from pending status based on their dependencies being completed. + If you have multiple operating systems in your Deadline installation, you will likely need to set up path mapping for machines to locate the script. + 5. Set the DEADLINE_DEPENDENCY_SCRIPT_PATH environment variable to the full path (including filename) where you saved the gaffer_batch_dependency.py file before running Gaffer. GafferDeadline dispatcher uses this variable as the location for the dependency script when submitting jobs to Deadline. + 6. Ensure that the DEADLINE_PATH environment variable is set to the directory where the "deadlinecommand" executable lives. This is typically set system-wide when you install the Deadline Client. GafferDeadline uses this environment variable to locate "deadlinecommand" for interacting with your Deadline repository. + +## Using ## +With everything set up correctly, Task Nodes in Gaffer will have a Deadline section on their Dispatcher tab. This section is where you setup the Deadline configuration for that task. You can set most common settings like groups, pools, priority, description, etc. + +When you are ready to submit the node(s) press the node's "Execute" button and select Deadline from the dropdown box of available dispatchers. + +You need a Deadline Client installed and connected to your repository on the machines you will be running GafferDeadline from. GafferDeadline uses the Deadline installation on the host machine, similar to other integrated submitters Deadline includes such as for Nuke, Houdini, etc. + +The Deadline settings in Gaffer include an override for the dependency method for that node. This override controls downstream Task Nodes that depend on the node on which it was set. Most of the time it should be left on Auto to let the dispatcher determine the most efficient method. If you know a node needs to be handled in a particular way, you can force its dendency method with the override plug. Usually the "Full Job" setting will be the safest but least flexible because downstream tasks will wait for all frames of that job to complete before being released. + +## Running Unit Tests ## +You don't need to run the unit tests for normal use of GafferDeadline, but if you want to make customizations it is recommended that you add unit tests as appropriate and run the existing tests to ensure compatibility. + +To run the unit tests, you need to have an installation of Gaffer and have your Python environment setup to point to that installation. The easiest way to do that is to use the included gaffer_env (Linux) and gaffer_env.bat (Windows) files to setup the environment first. Then you can use regular Python unit test runners to run tests. + +More specifically: +1. PATH environment variable needs to include the gaffer/bin, gaffer/lib (on Windows) and gaffer/python directories. +2. PYTHONPATH environment variable needs to include the gaffer/python directory. +3. On Linux the LD_LIBRARY_PATH needs to be set to the gaffer/lib directory. + +There is also a Visual Studio Code environment included that may be helpful. + +## Contributing ## +Feedback and pull requests are welcome! If you have ideas about how to improve the dispatcher, find bugs or would like to submit improvements, please create an issue on GitHub for discussion or a pull request. + +## Copyright and License ## + +© 2019 Hypothetical Inc. All rights reserved. + +Distributed under the [BSD license](LICENSE). \ No newline at end of file diff --git a/custom/Gaffer/gaffer.ico b/custom/Gaffer/gaffer.ico new file mode 100644 index 0000000..f8169fd Binary files /dev/null and b/custom/Gaffer/gaffer.ico differ diff --git a/custom/Gaffer/gaffer.options b/custom/Gaffer/gaffer.options new file mode 100644 index 0000000..0307ea6 --- /dev/null +++ b/custom/Gaffer/gaffer.options @@ -0,0 +1,60 @@ +[Script] +Type=string +Label=Script +Category=Gaffer Options +CategoryOrder=0 +Index=0 +Description=The Gaffer script to execute. +Required=true +DisableIfBlank=true + +[Version] +Type=label +Label=Version +Category=Gaffer Options +Index=1 +Description=The version of Gaffer to execute. +Required=false +DisableIfBlank=true + +[IgnoreScriptLoadErrors] +Type=boolean +Label=Ignore Script Load Errors +Category=Gaffer Options +CategoryOrder=0 +Index=2 +Description=Causes error which occur while load the script to be ignored. Not recommended. +Required=false +Default=false +DisableIfBlank=false + +[Nodes] +Type=string +Label=Nodes +Category=Gaffer Options +CategoryOrder=0 +Index=2 +Description=The names of the nodes to execute. If not specified then all executable nodes will be found automatically. +Required=false +DisableIfBlank=false + +[Frames] +Type=string +Label=Frames +Category=Gaffer Options +CategoryOrder=0 +Index=3 +Description=The frames to execute. The default value executes the current frame as stored in the script. +Required=false +DisableIfBlank=false +Default=false + +[Context] +Type=string +Label=Context +CategoryOrder=0 +Index=4 +Category=Gaffer Options +Description=The context used during the execution. Note that the frames parameter will be used to vary the context frame entry. +Required=false +DisableIfBlank=false diff --git a/custom/Gaffer/gaffer.param b/custom/Gaffer/gaffer.param new file mode 100644 index 0000000..125f8fc --- /dev/null +++ b/custom/Gaffer/gaffer.param @@ -0,0 +1,43 @@ +[About] +Type=label +Label=About +Category=About Plugin +CategoryOrder=-1 +Index=0 +Default=Gaffer for Deadline +Description=Not configurable + +[ConcurrentTasks] +Type=label +Label=ConcurrentTasks +Category=About Plugin +CategoryOrder=-1 +Index=0 +Default=True +Description=Not configurable. + +[Executable0_45_0_0] +Type=multilinemultifilename +Category=Gaffer 0.45.0.0 Executables +CategoryOrder=0 +CategoryIndex=0 +Label=Gaffer 0.45.0.0 Render Executable +Default=%HOME%/gaffer_0.45.0.0;~/gaffer_0.45.0.0 +Description=The path to the Gaffer 0.45.0.0 executable (gaffer.bat on Windows) file used for executing. Enter alternative paths on separate lines. +[Executable0_53_0_0] +Type=multilinemultifilename +Category=Gaffer 0.53.0.0 Executables +CategoryOrder=0 +CategoryIndex=0 +Label=Gaffer 0.53.0.0 Render Executable +Default=%HOME%/gaffer_0.53.0.0;~/gaffer_0.53.0.0 +Description=The path to the Gaffer 0.53.0.0 executable (gaffer.bat on Windows) file used for executing. Enter alternative paths on separate lines. + +[EnablePathMapping] +Type=boolean +Category=Path Mapping (For Mixed Farms) +CategoryOrder=70 +CategoryIndex=0 +Label=Enable Path Mapping +Default=true +Description=If enabled, environment variables will be set for the process running Gaffer. This feature can be turned off if there are no Path Mapping entries defined in the Repository Options. diff --git a/custom/Gaffer/gaffer.py b/custom/Gaffer/gaffer.py new file mode 100644 index 0000000..4e7b2a1 --- /dev/null +++ b/custom/Gaffer/gaffer.py @@ -0,0 +1,170 @@ +########################################################################## +# +# Copyright (c) 2019, Hypothetical Inc. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above +# copyright notice, this list of conditions and the following +# disclaimer. +# +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials provided with +# the distribution. +# +# * Neither the name of Hypothetical Inc. nor the names of +# any other contributors to this software may be used to endorse or +# promote products derived from this software without specific prior +# written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +########################################################################## + +import os +import re + +from System.IO import * +from System.Text.RegularExpressions import * + +from Deadline.Scripting import * +from Deadline.Plugins import * + +from FranticX.Processes import * + +###################################################################### +# This is the function that Deadline calls to get an instance of the +# main DeadlinePlugin class. +###################################################################### + + +def GetDeadlinePlugin(): + return GafferPlugin() + + +def CleanupDeadlinePlugin(deadlinePlugin): + deadlinePlugin.Cleanup() + +###################################################################### +# This is the main DeadlinePlugin class for the Gaffer plugin. +###################################################################### + + +class GafferPlugin(DeadlinePlugin): + def __init__(self): + self.InitializeProcessCallback += self.InitializeProcess + # self.RenderTasksCallback += self.RenderTasks + self.RenderExecutableCallback += self.GetRenderExecutable + self.RenderArgumentCallback += self.GetRenderArguments + # Some tasks like Ply2Vrmesh and Houdini sims handle multiple frames rather than a separate Deadline task per frame + self.currentFrame = 0.0 + self.totalFrames = 0.0 + + def Cleanup(self): + for stdoutHandler in self.StdoutHandlers: + del stdoutHandler.HandleCallback + + del self.InitializeProcessCallback + del self.RenderTasksCallback + + def InitializeProcess(self): + self.PluginType = PluginType.Simple + self.StdoutHandling = True + + # Generic Gaffer progress + self.AddStdoutHandlerCallback(".*Progress: (\d+)%.*").HandleCallback += self.HandleProgress + + # Vray's ply2vrmesh prints out lines for each frame and also each voxel within the frame + self.AddStdoutHandlerCallback(".*Subdividing frame ([0-9]+) of ([0-9]+).*").HandleCallback += self.HandlePly2VrmeshFrameProgress + self.AddStdoutHandlerCallback(".*Processing voxel ([0-9]+) of ([0-9]+).*").HandleCallback += self.HandlePly2VrmeshVoxelProgress + + def GetRenderExecutable(self): + self.Version = self.GetPluginInfoEntry("Version") + gafferExeList = self.GetConfigEntry("Executable" + str(self.Version).replace(".", "_")) + gafferExe = FileUtils.SearchFileList(gafferExeList) + if(gafferExe == ""): + self.FailRender("Gaffer %s render executable could not be found in the semicolon separated list \"%s\". The path to the render executable can be configured from the Plugin Configuration in the Deadline Monitor." % ( + self.Version, gafferExeList)) + + return gafferExe + + def GetRenderArguments(self): + script = RepositoryUtils.CheckPathMapping(self.GetPluginInfoEntryWithDefault("Script", "").strip()) + script = self.replaceSlashesByOS(script) + local_script = os.path.join(self.GetJobsDataDirectory(), script) + if os.path.isfile(local_script): + script = local_script + + ignoreErrors = self.GetPluginInfoEntryWithDefault("IgnoreScriptLoadErrors", "False") + nodes = self.GetPluginInfoEntryWithDefault("Nodes", "") + frames = self.GetPluginInfoEntryWithDefault("Frames", "") + frames = re.sub(r"<(?i)STARTFRAME>", str(self.GetStartFrame()), frames) + frames = re.sub(r"<(?i)ENDFRAME>", str(self.GetEndFrame()), frames) + frames = self.ReplacePaddedFrame(frames, "<(?i)STARTFRAME%([0-9]+)>", self.GetStartFrame()) + frames = self.ReplacePaddedFrame(frames, "<(?i)ENDFRAME%([0-9]+)>", self.GetEndFrame()) + context = self.GetPluginInfoEntryWithDefault("Context", "") + + arguments = "execute -script \"{}\"".format(script) + arguments += " -ignoreScriptLoadErrors" if ignoreErrors.lower() == "true" else "" + arguments += " -nodes {}".format(nodes) if nodes != "" else "" + arguments += " -frames {}".format(frames) if frames != "" else "" + arguments += " -context {}".format(context) if context != "" else "" + + return arguments + + def ReplacePaddedFrame(self, arguments, pattern, frame): + frameRegex = Regex(pattern) + while True: + frameMatch = frameRegex.Match(arguments) + if frameMatch.Success: + paddingSize = int(frameMatch.Groups[1].Value) + if paddingSize > 0: + padding = StringUtils.ToZeroPaddedString(frame, paddingSize, False) + else: + padding = str(frame) + arguments = arguments.replace(frameMatch.Groups[0].Value, padding) + else: + break + + return arguments + + def HandleProgress(self): + progress = float(self.GetRegexMatch(1)) + self.SetProgress(progress) + + def HandlePly2VrmeshFrameProgress(self): + self.currentFrame = float(self.GetRegexMatch(1)) - 1.0 + self.totalFrames = float(self.GetRegexMatch(2)) + + self.SetProgress(self.currentFrame / self.totalFrames * 100) + self.SetStatusMessage("Ply2Vrmesh: frame {}/{}".format(self.currentFrame, self.totalFrames)) + + def HandlePly2VrmeshVoxelProgress(self): + currentVoxel = float(self.GetRegexMatch(1)) - 1.0 + totalVoxels = float(self.GetRegexMatch(2)) + + voxelProgress = currentVoxel / totalVoxels + + self.SetProgress(((self.currentFrame / self.totalFrames) + (voxelProgress * 1.0 / self.totalFrames)) * 100) + self.SetStatusMessage("Ply2Vrmesh: Processing Voxel {}/{} @ frame {}/{}".format(currentVoxel, totalVoxels, self.currentFrame, self.totalFrames)) + + def replaceSlashesByOS(self, value): + if SystemUtils.IsRunningOnWindows(): + value = value.replace('/', '\\') + else: + value = value.replace("\\", "/") + + return value diff --git a/gaffer_batch_dependency.py b/gaffer_batch_dependency.py new file mode 100644 index 0000000..ba52664 --- /dev/null +++ b/gaffer_batch_dependency.py @@ -0,0 +1,123 @@ +########################################################################## +# +# Copyright (c) 2019, Hypothetical Inc. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above +# copyright notice, this list of conditions and the following +# disclaimer. +# +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials provided with +# the distribution. +# +# * Neither the name of Hypothetical Inc. nor the names of +# any other contributors to this software may be used to endorse or +# promote products derived from this software without specific prior +# written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +########################################################################## + +import re + +from Deadline.Scripting import * + +""" ExtraInfoKeyValue denote dependecies in the form of +:=> + +Deadline doesn't seem to include a logging facility for dependency scripts +so just using print for informative info in case jobs aren't releasing +as expected. +""" + +print_debug = False + +def __main__(jobID, taskIDs=None): + + # simple data structure to hold information about a dependency + class dependency(object): + def __init__(self, task, job_dep, dep_task, is_released): + self.task_id = int(task) + self.job_dependency_id = job_dep + self.dependency_task_id = int(dep_task) + self.is_released = is_released + + taskIDs = [int(t) for t in taskIDs] # Deadline gives task IDs in string format + + if taskIDs: + re_dep = re.compile(r'^([0-9]+):([a-z0-9]+)') + + job = RepositoryUtils.GetJob(jobID, False) + if print_debug: print "Checking dependencies for {}".format(job.JobName) + + dependencies = [] # list of dependency objects + job_dependency_ids = [] # list of job ids this job depends on + + # collect this job's dependencies + for k in job.GetJobExtraInfoKeys(): + result = re_dep.match(k) + if len(result.groups()) == 2: + task, job_dep = result.groups() + if int(task) in taskIDs: + task_dep = job.GetJobExtraInfoKeyValue(k) + new_dep = dependency(task, job_dep, task_dep, False) + job_dependency_ids.append(job_dep) + dependencies.append(new_dep) + + if print_debug: print("Found {} dependencies".format(len(dependencies))) + + job_dependency_ids = list(set(job_dependency_ids)) + # if no dependencies, release all tasks + if len(dependencies) == 0: + return taskIDs + + for job_dep_id in job_dependency_ids: + if print_debug: print "Scanning {} for released dependencies".format(job_dep_id) + job_dep_obj = RepositoryUtils.GetJob(job_dep, False) + # If the job can't be found, assume it is ok to release it's dependents + if job_dep_obj is None: + for d in dependencies: + if d.job_dependency_id == job_dep_id: + d.is_released = True + else: + job_dep_task_list = RepositoryUtils.GetJobTasks(job_dep_obj, False).TaskCollectionTasks + completed_tasks = [t for t in job_dep_task_list if t.TaskStatus.lower() == "completed"] + if print_debug: print "{} has {} completed tasks of {} total tasks: {}".format(job_dep_id, len(completed_tasks), len(job_dep_task_list), [t.TaskId for t in completed_tasks]) + for completed_task in completed_tasks: + for d in dependencies: + if d.job_dependency_id == job_dep_id and d.dependency_task_id == int(completed_task.TaskId): + d.is_released = True + print("{}:{} released".format(d.job_dependency_id, d.dependency_task_id)) + + released_tasks = [] + for task in dependencies: + if print_debug: print "Scanning task #{} for dependencies".format(task.task_id) + deps_for_this_task = list(set([t for t in dependencies if t.task_id == task.task_id])) + # print "Task #{} has {} dependencies: {}".format(task.task_id, len(deps_for_this_task), ",".join([d.dependency_task_id for d in deps_for_this_task])) + released_deps = list(set([d for d in deps_for_this_task if d.is_released])) + if print_debug: print "Task #{} has {} released dependencies".format(task.task_id, len(released_deps)) + if(len(deps_for_this_task) == len(released_deps)): + released_tasks.append(str(task.task_id)) + if print_debug: print "All dependencies for task #{} have been completed. Releasing task #{}".format(task.task_id, task.task_id) + + if print_debug: print("Released tasks for {} = {}".format(jobID, released_tasks)) + return list(set(released_tasks)) + + # not entirely sure what to do about a job that does not have frame dependencies enabled, that is considered an error state + return False diff --git a/gaffer_env b/gaffer_env new file mode 100644 index 0000000..42f7a3e --- /dev/null +++ b/gaffer_env @@ -0,0 +1,315 @@ +#! /bin/bash +########################################################################## +# +# Copyright (c) 2011-2012, John Haddon. All rights reserved. +# Copyright (c) 2011-2012, Image Engine Design Inc. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above +# copyright notice, this list of conditions and the following +# disclaimer. +# +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials provided with +# the distribution. +# +# * Neither the name of John Haddon nor the names of +# any other contributors to this software may be used to endorse or +# promote products derived from this software without specific prior +# written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +########################################################################## + +# Wrapper script for gaffer. This ensures that relevant environment +# variables are set appropriately and then runs gaffer.py in the +# correct python interpreter. +########################################################################## + +# set -e + +# Remove -psn_0 argument that the OS X launcher adds on annoyingly. +########################################################################## + +if [[ $1 == -psn_0_* ]] ; then + shift +fi + +# Work around OCIO bug that causes parsing to fail for certain locales. +# See https://github.com/imageworks/OpenColorIO/issues/297 +########################################################################## + +export LC_NUMERIC=C + +# # Find where this script is located, resolving any symlinks that were used +# # to invoke it. Set GAFFER_ROOT based on the script location. +# ########################################################################## + +# pushd . &> /dev/null + +# # resolve symlinks +# thisScript=$0 +# while [[ -L $thisScript ]] +# do +# cd "`dirname "$thisScript"`" +# thisScript=`basename "$thisScript"` +# thisScript=`readlink "$thisScript"` +# done + +# # find the bin directory we're in +# cd "`dirname "$thisScript"`" +# binDir=`pwd -P` +# export GAFFER_ROOT="`dirname "$binDir"`" + +# popd &> /dev/null + +# Make sure resource paths are set appropriately +########################################################################## + +# Prepend a directory to a path if it is not +# already there. +# +# $1 is the value to include in the path +# $2 is the name of the path to edit +# +# e.g. prependToPath "$HOME/bin" PATH +function prependToPath { + + if [[ ":${!2}:" != *":$1:"* ]] ; then + + if [[ ${!2} ]] ; then + eval "export $2=\"$1:${!2}\"" + else + eval "export $2=\"$1\"" + fi + + fi + +} + +function appendToPath { + + if [[ ":${!2}:" != *":$1:"* ]] ; then + + if [[ ${!2} ]] ; then + eval "export $2=\"${!2}:$1\"" + else + eval "export $2=\"$1\"" + fi + + fi + +} + +prependToPath "$GAFFER_ROOT/glsl" IECOREGL_SHADER_PATHS +prependToPath "$GAFFER_ROOT/glsl" IECOREGL_SHADER_INCLUDE_PATHS + +prependToPath "$GAFFER_ROOT/fonts" IECORE_FONT_PATHS +prependToPath "$HOME/gaffer/ops:$GAFFER_ROOT/ops" IECORE_OP_PATHS + +prependToPath "$HOME/gaffer/opPresets:$GAFFER_ROOT/opPresets" IECORE_OP_PRESET_PATHS +prependToPath "$HOME/gaffer/procedurals:$GAFFER_ROOT/procedurals" IECORE_PROCEDURAL_PATHS +prependToPath "$HOME/gaffer/proceduralPresets:$GAFFER_ROOT/proceduralPresets" IECORE_PROCEDURAL_PRESET_PATHS + +if [[ -z $CORTEX_POINTDISTRIBUTION_TILESET ]] ; then + export CORTEX_POINTDISTRIBUTION_TILESET="$GAFFER_ROOT/resources/cortex/tileset_2048.dat" +fi + +prependToPath "$HOME/gaffer/apps:$GAFFER_ROOT/apps" GAFFER_APP_PATHS + +prependToPath "$HOME/gaffer/startup" GAFFER_STARTUP_PATHS +appendToPath "$GAFFER_ROOT/startup" GAFFER_STARTUP_PATHS + +prependToPath "$HOME/gaffer/startup" CORTEX_STARTUP_PATHS +appendToPath "$GAFFER_ROOT/startup" CORTEX_STARTUP_PATHS + +prependToPath "$GAFFER_ROOT/graphics" GAFFERUI_IMAGE_PATHS + +if [[ -e $GAFFER_ROOT/bin/oslc ]] ; then + export OSLHOME=$GAFFER_ROOT +fi + +## \todo: should we rename this to "osl" to match our "glsl" folder? +prependToPath "$HOME/gaffer/shaders:$GAFFER_ROOT/shaders" OSL_SHADER_PATHS + +if [[ -z $GAFFEROSL_CODE_DIRECTORY ]] ; then + export GAFFEROSL_CODE_DIRECTORY="$HOME/gaffer/oslCode" + appendToPath "$GAFFEROSL_CODE_DIRECTORY" OSL_SHADER_PATHS +fi + +# Get python set up properly +########################################################################## + +# Make sure PYTHONHOME is pointing to our internal python build. +# We only do this if Gaffer has been built with an internal version +# of python - otherwise we assume the existing environment is providing +# the right value. + +if [[ -e $GAFFER_ROOT/bin/python ]] ; then + + if [[ `uname` = "Linux" ]] ; then + export PYTHONHOME="$GAFFER_ROOT" + else + export PYTHONHOME="$GAFFER_ROOT/lib/Python.framework/Versions/Current" + fi + +fi + +# Get python module path set up + +prependToPath "$GAFFER_ROOT/python" PYTHONPATH + +# Get library paths set up +########################################################################## + +if [[ `uname` = "Linux" ]] ; then + prependToPath "$GAFFER_ROOT/lib" LD_LIBRARY_PATH +else + prependToPath "$GAFFER_ROOT/lib" DYLD_FRAMEWORK_PATH + prependToPath "$GAFFER_ROOT/lib" DYLD_LIBRARY_PATH + # PySide2 installs its libraries next to its python modules, + # and messes up the rpaths such that they can't be relocated. + # Work around this by putting them on the DYLD_LIBRARY_PATH. + prependToPath "$PYTHONHOME/lib/python2.7/site-packages/PySide2" DYLD_LIBRARY_PATH + prependToPath /System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/ImageIO.framework/Resources/ DYLD_LIBRARY_PATH +fi + +if [[ -e $GAFFER_ROOT/qt/plugins ]] ; then + export QT_QPA_PLATFORM_PLUGIN_PATH="$GAFFER_ROOT/qt/plugins" +fi + +# Get the executable path set up, for running child processes from Gaffer +########################################################################## + +prependToPath "$GAFFER_ROOT/bin" PATH + +# Set up Appleseed +########################################################################## + +if [[ -z $APPLESEED && -d $GAFFER_ROOT/appleseed ]] ; then + + export APPLESEED="$GAFFER_ROOT/appleseed" + +fi + +if [[ $APPLESEED ]] ; then + + if [[ `uname` = "Linux" ]] ; then + prependToPath "$APPLESEED/lib" LD_LIBRARY_PATH + else + prependToPath "$APPLESEED/lib" DYLD_LIBRARY_PATH + fi + + # Using a glob to keep the wrapper agnostic of python version. + for appleseedPython in "$APPLESEED"/lib/python* ; do + prependToPath "$appleseedPython" PYTHONPATH + done + + prependToPath "$APPLESEED/shaders/gaffer" OSL_SHADER_PATHS + prependToPath "$APPLESEED/shaders/appleseed" OSL_SHADER_PATHS + prependToPath "$GAFFER_ROOT/appleseedDisplays" APPLESEED_SEARCHPATH + prependToPath "$OSL_SHADER_PATHS" APPLESEED_SEARCHPATH + + prependToPath "$APPLESEED/bin" PATH + +fi + +# Set up Arnold +########################################################################## + +prependToPath "$GAFFER_ROOT/arnold/plugins" ARNOLD_PLUGIN_PATH + +if [[ $ARNOLD_ROOT ]] ; then + + if [[ `uname` = "Linux" ]] ; then + appendToPath "$ARNOLD_ROOT/bin" LD_LIBRARY_PATH + else + appendToPath "$ARNOLD_ROOT/bin" DYLD_LIBRARY_PATH + fi + + appendToPath "$ARNOLD_ROOT/bin" PATH + appendToPath "$ARNOLD_ROOT/python" PYTHONPATH + +fi + +# Set up 3Delight +########################################################################## + +if [[ -n $DELIGHT ]] ; then + + if [[ `uname` = "Linux" ]] ; then + appendToPath "$DELIGHT/lib" LD_LIBRARY_PATH + else + appendToPath "$DELIGHT/lib" DYLD_LIBRARY_PATH + fi + + appendToPath "$DELIGHT/bin" PATH + appendToPath "$DELIGHT/python" PYTHONPATH + appendToPath "$DELIGHT/shaders" DL_SHADERS_PATH + appendToPath "$DELIGHT/displays" DL_DISPLAYS_PATH + + appendToPath "$DELIGHT" OSL_SHADER_PATHS + + appendToPath "$GAFFER_ROOT/renderMan/displayDrivers" DL_RESOURCES_PATH + +fi + +appendToPath "/Volumes/pipeline" PYTHONPATH +appendToPath "/Volumes/pipeline/gafferStartup" GAFFER_STARTUP_PATHS + +# Set up 3rd Party extensions +########################################################################## + +IFS=: +for extension in $GAFFER_EXTENSION_PATHS; do + + if [[ `uname` = "Linux" ]] ; then + appendToPath "$extension/lib" LD_LIBRARY_PATH + else + appendToPath "$extension/lib" DYLD_LIBRARY_PATH + fi + + appendToPath "$extension/bin" PATH + appendToPath "$extension/python" PYTHONPATH + appendToPath "$extension/apps" GAFFER_APP_PATHS + appendToPath "$extension/graphics" GAFFERUI_IMAGE_PATHS + appendToPath "$extension/glsl" IECOREGL_SHADER_PATHS + appendToPath "$extension/glsl" IECOREGL_SHADER_INCLUDE_PATHS + appendToPath "$extension/shaders" OSL_SHADER_PATHS + prependToPath "$extension/startup" GAFFER_STARTUP_PATHS + +done +unset IFS + +# Run gaffer itself +########################################################################## + +# if [[ -n $GAFFER_DEBUG ]] ; then +# if [[ -z $GAFFER_DEBUGGER ]] ; then +# if [[ `uname` = "Linux" ]] ; then +# export GAFFER_DEBUGGER="gdb --args" +# else +# export GAFFER_DEBUGGER="lldb -- " +# fi +# fi +# # Using `which` because lldb doesn't seem to respect $PATH +# exec $GAFFER_DEBUGGER `which python` "$GAFFER_ROOT/bin/gaffer.py" "$@" +# else +# exec python "$GAFFER_ROOT/bin/gaffer.py" "$@" +# fi +code diff --git a/gaffer_env.bat b/gaffer_env.bat new file mode 100644 index 0000000..38b0dc0 --- /dev/null +++ b/gaffer_env.bat @@ -0,0 +1,21 @@ +if "%GAFFER_ROOT%"=="" ( + echo GAFFER_ROOT environment variable not set + exit /B 1 +) + +if "%GAFFER_DEADLINE_PATH%" =="" ( + echo GAFFER_DEADLINE_PATH should be set to the path of the GafferDeadline installation + exit /B 1 +) + +set HOME=%USERPROFILE% + +set IECORE_FONT_PATHS=%GAFFER_ROOT%/fonts + +set PYTHONHOME=%GAFFER_ROOT% +set PYTHONPATH=%GAFFER_ROOT%\python;%PYTHONPATH% +set PATH=%GAFFER_ROOT%\lib;%PATH% +set PATH=%GAFFER_ROOT%\bin;%PATH% + +SET PYTHONPATH=%GAFFER_DEADLINE_PATH%\python;%PYTHONPATH% +SET DEADLINE_DEPENDENCY_SCRIPT_PATH=P:\command_scripts\gaffer_batch_dependency.py diff --git a/python/GafferDeadline/DeadlineDispatcher.py b/python/GafferDeadline/DeadlineDispatcher.py new file mode 100644 index 0000000..97e431a --- /dev/null +++ b/python/GafferDeadline/DeadlineDispatcher.py @@ -0,0 +1,380 @@ +########################################################################## +# +# Copyright (c) 2019, Hypothetical Inc. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above +# copyright notice, this list of conditions and the following +# disclaimer. +# +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials provided with +# the distribution. +# +# * Neither the name of Hypothetical Inc. nor the names of +# any other contributors to this software may be used to endorse or +# promote products derived from this software without specific prior +# written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +########################################################################## + +import os + +import IECore + +import Gaffer +import GafferDispatch + +import GafferDeadline + + +class DeadlineDispatcher(GafferDispatch.Dispatcher): + + def __init__(self, name="DeadlineDispatcher"): + GafferDispatch.Dispatcher.__init__(self, name) + self._deadline_jobs = [] + + # Emitted prior to submitting the Deadline job, to allow + # custom modifications to be applied. + # + # Slots should have the signature `slot( dispatcher, job )`, + # where dispatcher is the DeadlineDispatcher and job will + # be the instance of GafferDeadlineJob that is about + # to be spooled. + @classmethod + def preSpoolSignal(cls): + return cls.__preSpoolSignal + + __preSpoolSignal = Gaffer.Signal2() + + def _doDispatch(self, root_batch): + ''' + _doDispatch is called by Gaffer, the others (prefixed with __) are just helpers for Deadline + + Note that Gaffer and Deadline use some terms differently + Gaffer Batch =~ Deadline Task, which could be multiple frames in a single task. Depending on batch layout + multiple Deadline Tasks may be needed to fullfill a single Gaffer Batch. For example, a Deadline + Task can only handle sequential frames. + Gaffer TaskNode =~ Deadline Job. A Gaffer Task can have multiple Deadline Jobs to complete it depending on batch and context layout. + A DeadlineJob is defined by the combination of Gaffer TaskNode and Context. + Gaffer Job = set of Deadline Jobs (could be considered a Deadline Batch) + Use DeadlineJob, DeadlineTask, etc. to denote Deadline terminology and plain Batch, Job, etc. to denote Gaffer terminology. + + Batches can have dependencies completely independent of frame numbers. First + walk through the batch tree to build up a set of GafferDeadlineJob objects with GafferDeadlineTask objects corresponding + to the batches. + + When all tasks are created, go back through the tree to setup dependencies between tasks. Task dependencies may be different + from Batch dependencies because batches may have been split to accommodate Deadline's sequential frame task requirement. + + With dependencies set, start at the leaf nodes of the task tree (no upstream DeadlineJobs) and submit those first. That way + the Deadline Job ID can be stored and used by dependent jobs to set their dependencies correctly. + + To be compatible with Deadline's ExtraInfoKeyValue system, dependencies are reformatted at submission as + task:job_dependency_id=task_dependency_number + ''' + IECore.Log.info("Beginning Deadline submission") + dispatch_data = {} + dispatch_data["scriptNode"] = root_batch.preTasks()[0].node().scriptNode() + dispatch_data["scriptFile"] = os.path.join(self.jobDirectory(), os.path.basename(dispatch_data["scriptNode"]["fileName"].getValue()) or "untitled.gfr") + dispatch_data["scriptFile"] = dispatch_data["scriptFile"].replace("\\", os.sep).replace("/", os.sep) + + dispatch_data["scriptNode"].serialiseToFile(dispatch_data["scriptFile"]) + + context = Gaffer.Context.current() + dispatch_data["deadlineBatch"] = context.substitute(self["jobName"].getValue()) or "untitled" + + root_deadline_job = GafferDeadline.GafferDeadlineJob() + root_deadline_job.setGafferNode(root_batch.node()) + root_deadline_job.setAuxFiles([dispatch_data["scriptFile"]]) + self.__addGafferDeadlineJob(root_deadline_job) + root_jobs = [] + for upstream_batch in root_batch.preTasks(): + root_job = self.__buildDeadlineJobWalk(upstream_batch, dispatch_data) + if root_job is not None: + root_jobs.append(root_job) + + root_jobs = list(set(root_jobs)) + + for root_job in root_jobs: + self.__buildDeadlineDependencyWalk(root_job) + # Control jobs with nothing to control should be removed after the dependencies are set up. + # This mostly applies to FrameMask nodes where downstream nodes need to see tasks on the FrameMask + # to trigger Job-Job dependency mode, but those tasks should not be submitted to Deadline. + self.__removeOrphanTasksWalk(root_job) + self.__submitDeadlineJob(root_job, dispatch_data) + + def __buildDeadlineJobWalk(self, batch, dispatch_data): + if batch.blindData().get("deadlineDispatcher:visited"): + return self.__getGafferDeadlineJob(batch.node(), batch.context()) + + deadline_job = self.__getGafferDeadlineJob(batch.node(), batch.context()) + if not deadline_job: + deadline_job = GafferDeadline.GafferDeadlineJob() + deadline_job.setGafferNode(batch.node()) + deadline_job.setContext(batch.context()) + deadline_job.setAuxFiles([dispatch_data["scriptFile"]]) + self.__addGafferDeadlineJob(deadline_job) + + deadline_job.addBatch(batch, batch.frames()) + for upstream_batch in batch.preTasks(): + parent_deadline_job = self.__buildDeadlineJobWalk(upstream_batch, dispatch_data) + if parent_deadline_job is not None: + deadline_job.addParentJob(parent_deadline_job) + + batch.blindData()["deadlineDispatcher:visited"] = IECore.BoolData(True) + + return deadline_job + + def __buildDeadlineDependencyWalk(self, job): + for parent_job in job.getParentJobs(): + self.__buildDeadlineDependencyWalk(parent_job) + job.buildTaskDependencies() + + def __removeOrphanTasksWalk(self, job): + for parent_job in job.getParentJobs(): + self.__removeOrphanTasksWalk(parent_job) + job.removeOrphanTasks() + + def __getGafferDeadlineJob(self, node, context): + for j in self._deadline_jobs: + if j.getGafferNode() == node and j.getContext() == context: + return j + + def __addGafferDeadlineJob(self, new_deadline_job): + self._deadline_jobs.append(new_deadline_job) + self._deadline_jobs = list(set(self._deadline_jobs)) + + def __submitDeadlineJob(self, deadline_job, dispatch_data): + # submit jobs depth first so parent job IDs will be populated + for parent_job in deadline_job.getParentJobs(): + self.__submitDeadlineJob(parent_job, dispatch_data) + + gaffer_node = deadline_job.getGafferNode() + if gaffer_node is None or len(deadline_job.getTasks()) == 0 or len(deadline_job.getTasks()) == 0: + return None + + # this job is already submitted if it has an ID + if deadline_job.getJobID() is not None: + return deadline_job.getJobID() + + self.preSpoolSignal()(self, deadline_job) + + deadline_plug = gaffer_node["dispatcher"].getChild("deadline") + + if deadline_plug is not None: + initial_status = "Suspended" if deadline_plug["submitSuspended"].getValue() else "Active" + machine_list_type = "Blacklist" if deadline_plug["isBlackList"].getValue() else "Whitelist" + + # to prevent Deadline from splitting up our tasks (since we've already done that based on batches), set the chunk size to the largest frame range + chunk_size = deadline_job.getTasks()[0].getEndFrame() - deadline_job.getTasks()[0].getStartFrame() + 1 + frame_string = "" + for t in deadline_job.getTasks(): + chunk_size = max(t.getEndFrame() - t.getStartFrame() + 1, chunk_size) + if t.getStartFrame() == t.getEndFrame(): + frame_string += ",{}".format(t.getStartFrame()) + else: + frame_string += ",{}-{}".format(t.getStartFrame(), t.getEndFrame()) + + job_info = {"Name": gaffer_node.relativeName(dispatch_data["scriptNode"]), + "Frames": frame_string, + "ChunkSize": chunk_size, + "Plugin": "Gaffer", + "BatchName": dispatch_data["deadlineBatch"], + "Comment": deadline_plug["comment"].getValue(), + "Department": deadline_plug["department"].getValue(), + "Pool": deadline_plug["pool"].getValue(), + "SecondaryPool": deadline_plug["secondaryPool"].getValue(), + "Group": deadline_plug["group"].getValue(), + "Priority": deadline_plug["priority"].getValue(), + "TaskTimeoutMinutes": int(deadline_plug["taskTimeout"].getValue()), + "EnableAutoTimeout": deadline_plug["enableAutoTimeout"].getValue(), + "ConcurrentTasks": deadline_plug["concurrentTasks"].getValue(), + "MachineLimit": deadline_plug["machineLimit"].getValue(), + machine_list_type: deadline_plug["machineList"].getValue(), + "LimitGroups": deadline_plug["limits"].getValue(), + "OnJobComplete": deadline_plug["onJobComplete"].getValue(), + "InitialStatus": initial_status, + } + + """ Dependencies are stored with a reference to the Deadline job since job IDs weren't assigned + when the task tree was walked. Now that parent jobs have been submitted and have IDs, + we can substitute that in for the dependency script to pick up. + + We also want to dependencies to be as native to Deadline as possible, resorting to the dependency + script only in cases where it is needed (Deadline's dependency script triggering seems to be slower + than native task dependencies) + + There are three possible dependency types allowed by Deadline: + 1) Job-Job: All of the tasks for job A wait for all of the tasks for job B to finish before job A runs. + This is relatively rare when coming from Deadline and mostly is used by nodes upstream from + a FrameMask node. In that case releasing tasks per-frame would trigger downstream jobs sooner + than they should. + 2) Frame-Frame: This is somewhat misleadingly named because Deadline only checks for frame dependency release + after each task completes, so this is very similar to task-task dependencies. Deadline can + only handle a start and end frame offset when comparing to the parent job so the task + offsets must match across all parent jobs to enable this mode. + 3) Task-Task: A task for job A waits for a task for job B to finish before the task for job A runs. + If the dependency start and end frame offsets don't match, this has to be handled by a + dependency script. + """ + dep_list = deadline_job.getDependencies() + + if len(dep_list) > 0 and deadline_plug["dependencyMode"].getValue() != "None": + job_dependent = False + frame_dependent = False + simple_frame_offset = True + if deadline_plug["dependencyMode"].getValue() == "Job": + job_dependent = True + elif deadline_plug["dependencyMode"].getValue() == "Frame": + frame_dependent = True + elif deadline_plug["dependencyMode"].getValue() == "Auto": + job_dependent = False + dep_jobs = [j["dependency_job"] for j in dep_list] + dep_jobs = list(set(dep_jobs)) + for dep in dep_list: + if dep["dependency_task"].getStartFrame() is None or dep["dependency_task"].getEndFrame() is None: + job_dependent = True + + frame_dependent = True + simple_frame_offset = True + if len(dep_list) > 0 and dep_list[0]["dependency_task"].getStartFrame() is not None and dep_list[0]["dependency_task"].getEndFrame() is not None: + deadline_job._frame_dependency_offset_start = dep_list[0]["dependency_task"].getStartFrame() - dep_list[0]["dependent_task"].getStartFrame() + deadline_job._frame_dependency_offset_end = dep_list[0]["dependency_task"].getEndFrame() - dep_list[0]["dependent_task"].getEndFrame() + + for dep_task in dep_list: + new_frame_offset_start = dep_task["dependency_task"].getStartFrame() - dep_task["dependent_task"].getStartFrame() + new_frame_offset_end= dep_task["dependency_task"].getEndFrame() - dep_task["dependent_task"].getEndFrame() + + if new_frame_offset_start != deadline_job._frame_dependency_offset_start or new_frame_offset_end != deadline_job._frame_dependency_offset_end: + simple_frame_offset = False + + # If we can't just shift the frame start and end, we might still be able to use frame dependency with tasks of different frame lengths + if not simple_frame_offset: + dep_jobs = [j["dependency_job"] for j in dep_list] + dep_jobs = list(set(dep_jobs)) + for dep_job in dep_jobs: + dep_tasks = [t["dependency_task"] for t in dep_list if t["dependency_job"] == dep_job] + dep_frames = [] + for t in dep_tasks: + if t.getStartFrame() is not None and t.getEndFrame() is not None: + dep_frames += range(t.getStartFrame(), t.getEndFrame() + 1) + current_tasks = [t["dependent_task"] for t in dep_list if t["dependency_job"] == dep_job] + current_frames = [] + for t in current_tasks: + if t.getStartFrame() is not None and t.getEndFrame() is not None: + current_frames += range(t.getStartFrame(), t.getEndFrame() + 1) + for f in current_frames: + if f not in dep_frames: + frame_dependent = False + + else: + frame_dependent = False + + if job_dependent or frame_dependent: + job_info.update( + { + "JobDependencies": ",".join(list(set([j["dependency_job"].getJobID() for j in dep_list]))), + "ResumeOnDeletedDependencies": True, + } + ) + if simple_frame_offset: + job_info.update( + { + "FrameDependencyOffsetStart": deadline_job._frame_dependency_offset_start, + "FrameDependencyOffsetEnd": deadline_job._frame_dependency_offset_end, + } + ) + if frame_dependent: + job_info.update({"IsFrameDependent": True}) + deadline_job.setDependencyType(GafferDeadline.GafferDeadlineJob.DeadlineDependencyType.frame_to_frame) + else: + deadline_job.setDependencyType(GafferDeadline.GafferDeadlineJob.DeadlineDependencyType.job_to_job) + else: + job_info.update( + { + "ScriptDependencies": os.environ["DEADLINE_DEPENDENCY_SCRIPT_PATH"], + "IsFrameDependent": True, + } + ) + for i in range(0,len(dep_list)): + dep_task = dep_list[i] + job_info["ExtraInfoKeyValue{}".format(i)] = "{}:{}={}".format(int(dep_task["dependent_task"].getTaskNumber()), dep_task["dependency_job"].getJobID(), dep_task["dependency_task"].getTaskNumber()) + deadline_job.setDependencyType(GafferDeadline.GafferDeadlineJob.DeadlineDependencyType.scripted) + else: + deadline_job.setDependencyType(GafferDeadline.GafferDeadlineJob.DeadlineDependencyType.none) + + plugin_info = {"Script": os.path.split(dispatch_data["scriptFile"])[-1], + "Version": Gaffer.About.versionString(), + "IgnoreScriptLoadErrors": False, + "Nodes": gaffer_node.relativeName(dispatch_data["scriptNode"]), + "Frames": "-", + } + scriptContext = dispatch_data["scriptNode"].context() + contextArgs = [] + for entry in [k for k in deadline_job.getContext().keys() if k != "frame" and not k.startswith("ui:")]: + if entry not in scriptContext.keys() or deadline_job.getContext()[entry] != scriptContext[entry]: + contextArgs.extend(["\"-{}\"".format(entry), "\"{}\"".format(repr(deadline_job.getContext()[entry]))]) + if contextArgs: + plugin_info["Context"] = " ".join(contextArgs) + deadline_job.setJobProperties(job_info) + deadline_job.setPluginProperties(plugin_info) + job_file_path = os.path.join(os.path.split(dispatch_data["scriptFile"])[0], gaffer_node.relativeName(dispatch_data["scriptNode"]) + ".job") + plugin_file_path = os.path.join(os.path.split(dispatch_data["scriptFile"])[0], gaffer_node.relativeName(dispatch_data["scriptNode"]) + ".plugin") + + job_id, output = deadline_job.submitJob(job_file_path=job_file_path, plugin_file_path=plugin_file_path) + if job_id is None: + IECore.Log.error(job_info["Name"], "failed to submit to Deadline.", output) + else: + IECore.Log.info(job_info["Name"], "submission succeeded.", output) + + return deadline_job.getJobID() + else: + print "oh no!" + return None + + @staticmethod + def _setupPlugs(parent_plug): + + if "deadline" in parent_plug: + return + + parent_plug["deadline"] = Gaffer.Plug() + parent_plug["deadline"]["comment"] = Gaffer.StringPlug() + parent_plug["deadline"]["department"] = Gaffer.StringPlug() + parent_plug["deadline"]["pool"] = Gaffer.StringPlug() + parent_plug["deadline"]["secondaryPool"] = Gaffer.StringPlug() + parent_plug["deadline"]["group"] = Gaffer.StringPlug() + parent_plug["deadline"]["priority"] = Gaffer.IntPlug(defaultValue=50, minValue=0, maxValue=100) + parent_plug["deadline"]["taskTimeout"] = Gaffer.IntPlug(defaultValue=0, minValue=0) + parent_plug["deadline"]["enableAutoTimeout"] = Gaffer.BoolPlug(defaultValue=False) + parent_plug["deadline"]["concurrentTasks"] = Gaffer.IntPlug(defaultValue=1, minValue=1, maxValue=16) + parent_plug["deadline"]["machineLimit"] = Gaffer.IntPlug(defaultValue=0, minValue=0) + parent_plug["deadline"]["machineList"] = Gaffer.StringPlug() + parent_plug["deadline"]["isBlackList"] = Gaffer.BoolPlug(defaultValue=False) + parent_plug["deadline"]["limits"] = Gaffer.StringPlug() + parent_plug["deadline"]["onJobComplete"] = Gaffer.StringPlug() + parent_plug["deadline"]["onJobComplete"].setValue("Nothing") + parent_plug["deadline"]["submitSuspended"] = Gaffer.BoolPlug(defaultValue=False) + parent_plug["deadline"]["dependencyMode"] = Gaffer.StringPlug() + parent_plug["deadline"]["dependencyMode"].setValue("Auto") + +IECore.registerRunTimeTyped(DeadlineDispatcher, typeName="GafferDeadline::DeadlineDispatcher") + +GafferDispatch.Dispatcher.registerDispatcher("Deadline", DeadlineDispatcher, DeadlineDispatcher._setupPlugs) diff --git a/python/GafferDeadline/DeadlineTools.py b/python/GafferDeadline/DeadlineTools.py new file mode 100644 index 0000000..da47865 --- /dev/null +++ b/python/GafferDeadline/DeadlineTools.py @@ -0,0 +1,90 @@ +########################################################################## +# +# Copyright (c) 2019, Hypothetical Inc. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above +# copyright notice, this list of conditions and the following +# disclaimer. +# +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials provided with +# the distribution. +# +# * Neither the name of Hypothetical Inc. nor the names of +# any other contributors to this software may be used to endorse or +# promote products derived from this software without specific prior +# written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +########################################################################## + +import os +import subprocess +import re + +import IECore + + +def runDeadlineCommand(arguments, hideWindow=True): + if "DEADLINE_PATH" not in os.environ: + raise(RuntimeError, "DEADLINE_PATH must be set to the Deadline executable path") + executable_suffix = ".exe" if os.name == "nt" else "" + deadline_command = os.path.join(os.environ['DEADLINE_PATH'], "deadlinecommand" + executable_suffix) + + arguments = [deadline_command] + arguments + + p = subprocess.Popen(arguments, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + + output, err = p.communicate() + + if err: + raise(RuntimeError, "Error running Deadline command {}: {}".format(" ".join(arguments), output)) + + return output + + +def submitJob(job_info_file, plugin_info_file, aux_files): + submission_results = runDeadlineCommand([job_info_file, plugin_info_file] + aux_files) + + for line in submission_results.split(): + if line.startswith("JobID="): + jobID = line.replace("JobID=", "").strip() + return (jobID, submission_results) + + return (None, submission_results) + + +def getMachineList(): + output = runDeadlineCommand(["GetSlaveNames"]) + return output.split() + + +def getLimitGroups(): + output = runDeadlineCommand(["GetLimitGroups"]) + return re.findall(r'Name=(.*)', output) + + +def getGroups(): + output = runDeadlineCommand(["GetSubmissionInfo", "groups"]) + return output.split()[1:] # remove [Groups] header + + +def getPools(): + output = runDeadlineCommand(["GetSubmissionInfo", "pools"]) + return output.split()[1:] # remove [Groups] header diff --git a/python/GafferDeadline/GafferDeadlineJob.py b/python/GafferDeadline/GafferDeadlineJob.py new file mode 100644 index 0000000..549c311 --- /dev/null +++ b/python/GafferDeadline/GafferDeadlineJob.py @@ -0,0 +1,247 @@ +########################################################################## +# +# Copyright (c) 2019, Hypothetical Inc. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above +# copyright notice, this list of conditions and the following +# disclaimer. +# +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials provided with +# the distribution. +# +# * Neither the name of Hypothetical Inc. nor the names of +# any other contributors to this software may be used to endorse or +# promote products derived from this software without specific prior +# written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +########################################################################## + +import os +import tempfile + +import IECore + +import GafferDispatch +import GafferDeadline +import DeadlineTools + + +class GafferDeadlineJob(object): + """ Manage simple aspects of a Deadline job for use with GafferDeadline. + This is not meant to be comprehensive but only to provide the functionality + to submit jobs and keep track of their job ids after submission. Hard coded + for Gaffer plugin Deadline jobs for simplicity. + """ + + class DeadlineDependencyType(object): + none = 0 + job_to_job = 1 + frame_to_frame = 2 + scripted = 3 + + def __init__(self, job_properties={}, plugin_properties={}, aux_files=[], gaffer_node=None, job_context={}, chunk_size=1): + self._dependency_type = None + self._frame_dependency_offset_start = 0 + self._frame_dependency_offset_end = 0 + + self.setJobProperties(job_properties) + self.setPluginProperties(plugin_properties) + self.setAuxFiles(aux_files) + self.setGafferNode(gaffer_node) + self.setContext(job_context) + + self._job_id = None + self._parent_jobs = [] + self._tasks = [] + # dependencies are of the dictionary form {"dependency_job": , "dependent_task": , "dependency_task": ) + self._dependencies = [] + + def setJobProperties(self, new_properties): + """ The only parameter Deadline requires is Plugin and because we are + focusing on Gaffer plugins, make sure that's always set. + """ + assert(type(new_properties) == dict) + self._job_properties = new_properties + self._job_properties.update({"Plugin": "Gaffer"}) + + def getJobProperties(self): + return self._job_properties + + def setDependencyType(self, dep_type): + self._dependency_type = dep_type + + def getDependencyType(self): + return self._dependency_type + + def setPluginProperties(self, new_properties): + assert(type(new_properties) == dict) + self._plugin_properties = new_properties + + def getPluginProperties(self): + return self._plugin_properties + + def setAuxFiles(self, new_aux_files): + assert(type(new_aux_files) == list or type(new_aux_files) == str) + new_aux_files = new_aux_files if type(new_aux_files) == list else [new_aux_files] + self._aux_files = new_aux_files + + def getAuxFiles(self): + return self._aux_files + + def getJobID(self): + return self._job_id + + def setGafferNode(self, new_node): + if not issubclass(type(new_node), GafferDispatch.TaskNode) and new_node is not None: + raise (ValueError, "Gaffer node must be a GafferDispatch.TaskNode or None") + self._gaffer_node = new_node + + def getGafferNode(self): + return self._gaffer_node + + def setContext(self, new_context): + self._context = new_context + + def getContext(self): + return self._context + + def addParentJob(self, parent_job): + if type(parent_job) != GafferDeadlineJob: + raise (ValueError, "Parent job must be a GafferDeadlineJob") + if parent_job not in self._parent_jobs: + self._parent_jobs.append(parent_job) + + def getParentJobs(self): + return self._parent_jobs + + def getParentJobByGafferNode(self, gaffer_node): + for job in self._parent_jobs: + if job.getGafferNode() == gaffer_node: + return job + + return None + + # Separate batch_frames out so it can be unit tested. Gaffer does not allow creating _TaskBatch objects + def addBatch(self, new_batch, batch_frames): + """ A batch corresponds to one or more Deadline Tasks + Deadline Tasks must be sequential frames with only a start and end frame + """ + assert(new_batch is None or type(new_batch) == GafferDispatch.Dispatcher._TaskBatch) + # some TaskNodes like TaskList and TaskWedge submit with no frames because they are just hierarchy placeholders + # they still need to be in for proper dependency handling + if len(batch_frames) > 0: + current_task = GafferDeadline.GafferDeadlineTask(new_batch, len(self.getTasks()), start_frame=batch_frames[0], end_frame=batch_frames[0]) + self._tasks.append(current_task) + for i in range(1, len(batch_frames)): + if (batch_frames[i] - batch_frames[i-1]) > 1: + current_task = GafferDeadline.GafferDeadlineTask(new_batch, len(self.getTasks()), start_frame=batch_frames[i], end_frame=batch_frames[i]) + self._tasks.append(current_task) + else: + current_task.setEndFrame(batch_frames[i]) + else: + # Control nodes like TaskList have no frames but do need tasks created to pass + # through dependencies + self._tasks.append(GafferDeadline.GafferDeadlineTask(new_batch, len(self.getTasks()))) + + def getTasksForBatch(self, batch): + task_list = [t for t in self.getTasks() if t.getGafferBatch() == batch] + return task_list + + def getTasks(self): + return self._tasks + + def buildTaskDependencies(self): + """ Link tasks to each other via their batch. Batches come from Gaffer and are what ultimately + need to be linked. But there may be more than one task for a particular batch. + """ + for task in self.getTasks(): + for job in self._parent_jobs: + for parent_batch in task.getGafferBatch().preTasks(): + dependency_tasks = job.getTasksForBatch(parent_batch) + for dep in dependency_tasks: + # Control tasks should have their frames set to the upstream frame range + # effectively making them frame-frame dependent + if task.getStartFrame() is None or task.getEndFrame() is None: + task.setFrameRange(dep.getStartFrame(), dep.getEndFrame()) + dep_dict = { + "dependency_job": job, + "dependent_task": task, + "dependency_task": dep + } + self._dependencies.append(dep_dict) + + def removeOrphanTasks(self): + self._tasks = [t for t in self.getTasks() if t.getStartFrame() is not None or t.getEndFrame() is not None or len(t.getGafferBatch().preTasks()) > 0] + + def getDependencies(self): + return self._dependencies + + def submitJob(self, job_file_path=None, plugin_file_path=None): + """ Submit the job to Deadline. + Returns a tuple of (submitted_job_id, Deadline_status_output). submitted_job_id + will be None if submission failed. Deadline_status_output can be used to help figure out + why it failed. + + Check to make sure that all auxiliary files exist, otherwise submission will fail + Job and plugin information are stored in temporary files that are deleted after submission. + Windows has a problem with allowing Python to hide the temp file from the OS, + so the delete=False argument must be passed. + + Job and plugin files are just serializations of their respective dictionaries in the + form of key=value separated by newlines. + + If the job_file or plugin_file are not supplied, create temp files for them. + Only remove temp files, not supplied files. + """ + for aux_file in self._aux_files: + if not os.path.isfile(aux_file): + raise IOError("{} does not exist".format(aux_file)) + + if job_file_path is None: + job_file = tempfile.NamedTemporaryFile(mode="w", suffix=".info", delete=False) + else: + job_file = open(job_file_path, mode="w") + job_lines = ["{}={}".format(k, self._job_properties[k]) for k in self._job_properties.keys()] + job_file.write("\n".join(job_lines)) + job_file.close() + + if plugin_file_path is None: + plugin_file = tempfile.NamedTemporaryFile(mode="w", suffix=".info", delete=False) + else: + plugin_file = open(plugin_file_path, mode="w") + plugin_lines = ["{}={}".format(k, self._plugin_properties[k]) for k in self._plugin_properties.keys()] + plugin_file.write("\n".join(plugin_lines)) + plugin_file.close() + + result = DeadlineTools.submitJob(job_file.name, plugin_file.name, self._aux_files) + + IECore.Log.debug("Submission results:", result) + + if result[0] is None: + raise(RuntimeError, "Deadline submission failed: \n{}".format(result[1])) + self._job_id = result[0] + + if job_file is None: + os.remove(job_file) + if plugin_file is None: + os.remove(plugin_file) + + return (self._job_id, result[1]) diff --git a/python/GafferDeadline/GafferDeadlineTask.py b/python/GafferDeadline/GafferDeadlineTask.py new file mode 100644 index 0000000..62b612b --- /dev/null +++ b/python/GafferDeadline/GafferDeadlineTask.py @@ -0,0 +1,123 @@ +########################################################################## +# +# Copyright (c) 2019, Hypothetical Inc. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above +# copyright notice, this list of conditions and the following +# disclaimer. +# +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials provided with +# the distribution. +# +# * Neither the name of Hypothetical Inc. nor the names of +# any other contributors to this software may be used to endorse or +# promote products derived from this software without specific prior +# written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +########################################################################## + +import GafferDispatch + +class GafferDeadlineTask(object): + """ Mimic the Deadline representation of a task: + - tasks are a sequential range of frames indicated by the start frame and end frame + - tasks can only be associated with one job and therefore one batch / Gaffer Task Node + """ + def __init__(self, gaffer_batch, task_number, start_frame=None, end_frame=None): + self._start_frame = None + self._end_frame = None + + self.setGafferBatch(gaffer_batch) + self.setStartFrame(start_frame) + self.setEndFrame(end_frame) + self.setTaskNumber(task_number) + + if self._start_frame is None and self.getGafferBatch() is not None and len(self.getGafferBatch().frames()) > 0: + self.setStartFrame(gaffer_batch.frames()[0]) + if self._end_frame is None and self.getGafferBatch() is not None and len(self.getGafferBatch().frames()) > 0: + self.setEndFrame(gaffer_batch.frames()[len(gaffer_batch.frames()) - 1]) + + def setTaskNumber(self, task_number): + assert(type(task_number) == int) + self._task_number = task_number + + def getTaskNumber(self): + return self._task_number + + def setGafferBatch(self, gaffer_batch): + assert(gaffer_batch is None or type(gaffer_batch) == GafferDispatch.Dispatcher._TaskBatch) + self._gaffer_batch = gaffer_batch + + def getGafferBatch(self): + return self._gaffer_batch + + def setFrameRange(self, start_frame, end_frame): + if end_frame < start_frame: + raise (ValueError, "End frame must be greater than start frame.") + if int(start_frame) != start_frame or int(end_frame) != end_frame: + raise (ValueError, "Start and end frames must be integers.") + self._start_frame = int(start_frame) + self._end_frame = int(end_frame) + + def setFrameRangeFromList(self, frame_list): + frames_sequential = True + if len(frame_list) > 0: + if int(frame_list[0]) != frame_list[0]: + raise(ValueError, "Frame numbers must be integers.") + for i in range(1, len(frame_list)-1): + if int(frame_list[i]) != frame_list[i]: + raise(ValueError, "Frame numbers must be integers.") + if frame_list[i] - frame_list[i-1] != 1: + frames_sequential = False + + if not frames_sequential: + raise (ValueError, "Frame list must be sequential.") + self._start_frame = int(frame_list[0]) + self._end_frame = int(frame_list[len(frame_list) - 1]) + else: + self.setStartFrame(None) + self.setEndFrame(None) + + def setStartFrame(self, start_frame): + if self._end_frame is not None and start_frame is not None and start_frame > self._end_frame: + raise(ValueError, "Start frame must be less than end frame.") + if start_frame is not None: + if int(start_frame) != start_frame: + raise(ValueError, "Frame numbers must be integers.") + self._start_frame = int(start_frame) + else: + self._start_frame = None + + def getStartFrame(self): + return self._start_frame + + def setEndFrame(self, end_frame): + if self._start_frame is not None and end_frame is not None and end_frame < self._start_frame: + raise(ValueError, "End frame must be greater than start frame.") + if end_frame is not None: + if int(end_frame) != end_frame: + raise(ValueError, "Frame numbers must be integers.") + self._end_frame = int(end_frame) + else: + self._end_frame = None + + def getEndFrame(self): + return self._end_frame diff --git a/python/GafferDeadline/__init__.py b/python/GafferDeadline/__init__.py new file mode 100644 index 0000000..ac8e491 --- /dev/null +++ b/python/GafferDeadline/__init__.py @@ -0,0 +1,42 @@ +########################################################################## +# +# Copyright (c) 2019, Hypothetical Inc. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above +# copyright notice, this list of conditions and the following +# disclaimer. +# +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials provided with +# the distribution. +# +# * Neither the name of Hypothetical Inc. nor the names of +# any other contributors to this software may be used to endorse or +# promote products derived from this software without specific prior +# written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +########################################################################## + +from DeadlineDispatcher import DeadlineDispatcher +from GafferDeadlineJob import GafferDeadlineJob +from GafferDeadlineTask import GafferDeadlineTask +from DeadlineTools import * + +__import__( "IECore" ).loadConfig( "GAFFER_STARTUP_PATHS", {}, subdirectory = "GafferDeadline" ) diff --git a/python/GafferDeadlineTest/DeadlineDispatcherTest.py b/python/GafferDeadlineTest/DeadlineDispatcherTest.py new file mode 100644 index 0000000..4f007aa --- /dev/null +++ b/python/GafferDeadlineTest/DeadlineDispatcherTest.py @@ -0,0 +1,761 @@ +########################################################################## +# +# Copyright (c) 2019, Hypothetical Inc. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above +# copyright notice, this list of conditions and the following +# disclaimer. +# +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials provided with +# the distribution. +# +# * Neither the name of Hypothetical Inc. nor the names of +# any other contributors to this software may be used to endorse or +# promote products derived from this software without specific prior +# written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +########################################################################## + +import os +import unittest +import mock + +import Gaffer +import GafferTest +import GafferDispatch +import GafferDispatchTest +import GafferDeadline + + +class DeadlineDispatcherTest(GafferTest.TestCase): + def __dispatcher(self): + dispatcher = GafferDeadline.DeadlineDispatcher() + dispatcher["jobsDirectory"].setValue(os.path.join(self.temporaryDirectory(), "testJobDirectory").replace("\\", "\\\\")) + + return dispatcher + + def __job(self, nodes, dispatcher=None): + jobs = [] + + def f(dispatcher, job): + jobs.append(job) + + c = GafferDeadline.DeadlineDispatcher.preSpoolSignal().connect(f) + + if dispatcher is None: + dispatcher = self.__dispatcher() + + dispatcher.dispatch(nodes) + + return jobs + + def testPreSpoolSignal(self): + s = Gaffer.ScriptNode() + s["n"] = GafferDispatchTest.LoggingTaskNode() + + spooled = [] + + def f(dispatcher, job): + spooled.append((dispatcher, job)) + + c = GafferDeadline.DeadlineDispatcher.preSpoolSignal().connect(f) + + dispatcher = self.__dispatcher() + with mock.patch('GafferDeadline.DeadlineTools.submitJob', return_value=("testID", "testMessage")): + dispatcher.dispatch([s["n"]]) + + self.assertEqual(len(spooled), 1) + self.assertTrue(spooled[0][0] is dispatcher) + + def testJobScript(self): + s = Gaffer.ScriptNode() + s["n"] = GafferDispatchTest.LoggingTaskNode() + + dispatcher = self.__dispatcher() + with mock.patch('GafferDeadline.DeadlineTools.submitJob', return_value=("testID", "testMessage")): + dispatcher.dispatch([s["n"]]) + + self.assertTrue(os.path.isfile(os.path.join(dispatcher.jobDirectory(), "n.job"))) + self.assertTrue(os.path.isfile(os.path.join(dispatcher.jobDirectory(), "n.plugin"))) + + def testTaskAttributes(self): + s = Gaffer.ScriptNode() + s["n"] = GafferDispatchTest.LoggingTaskNode() + s["n"]["frame"] = Gaffer.StringPlug(defaultValue="${frame}", flags=Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic) + s["n"]["dispatcher"]["batchSize"].setValue(10) + + dispatcher = self.__dispatcher() + dispatcher["framesMode"].setValue(dispatcher.FramesMode.CustomRange) + dispatcher["frameRange"].setValue("1-10") + + with mock.patch('GafferDeadline.DeadlineTools.submitJob', return_value=("testID", "testMessage")): + jobs = self.__job([s["n"]], dispatcher) + self.assertEqual(len(jobs[0].getParentJobs()), 0) + self.assertEqual(len(jobs[0].getTasks()), 1) + + def testMultipleBatches(self): + s = Gaffer.ScriptNode() + s["n"] = GafferDispatchTest.LoggingTaskNode() + s["n"]["frame"] = Gaffer.StringPlug(defaultValue="${frame}", flags=Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic) + s["n"]["dispatcher"]["batchSize"].setValue(1) + + dispatcher = self.__dispatcher() + dispatcher["framesMode"].setValue(dispatcher.FramesMode.CustomRange) + dispatcher["frameRange"].setValue("1-10") + + with mock.patch('GafferDeadline.DeadlineTools.submitJob', return_value=("testID", "testMessage")): + jobs = self.__job([s["n"]], dispatcher) + + self.assertEqual(len(jobs), 1) + self.assertEqual(len(jobs[0].getParentJobs()), 0) + self.assertEqual(len(jobs[0].getTasks()), 10) + + def testSingleBatch(self): + s = Gaffer.ScriptNode() + s["n"] = GafferDispatchTest.LoggingTaskNode() + s["n"]["frame"] = Gaffer.StringPlug(defaultValue="${frame}", flags=Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic) + s["n"]["dispatcher"]["batchSize"].setValue(10) + + dispatcher = self.__dispatcher() + dispatcher["framesMode"].setValue(dispatcher.FramesMode.CustomRange) + dispatcher["frameRange"].setValue("1-10") + + with mock.patch('GafferDeadline.DeadlineTools.submitJob', return_value=("testID", "testMessage")): + jobs = self.__job([s["n"]], dispatcher) + + self.assertEqual(len(jobs), 1) + self.assertEqual(len(jobs[0].getTasks()), 1) + + def testCollapseIdenticalFrames(self): + s = Gaffer.ScriptNode() + s["n"] = GafferDispatch.PythonCommand() + s["n"]["command"] = Gaffer.StringPlug(defaultValue="print(\"Hello\")", flags=Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic) + s["n"]["dispatcher"]["batchSize"].setValue(1) + + dispatcher = self.__dispatcher() + dispatcher["framesMode"].setValue(dispatcher.FramesMode.CustomRange) + dispatcher["frameRange"].setValue("1-10") + + with mock.patch('GafferDeadline.DeadlineTools.submitJob', return_value=("testID", "testMessage")): + jobs = self.__job([s["n"]], dispatcher) + + self.assertEqual(len(jobs), 1) + self.assertEqual(len(jobs[0].getTasks()), 1) + + def testPreTasks(self): + # n1 + # | + # n2 n3 + + s = Gaffer.ScriptNode() + s["n1"] = GafferDispatchTest.LoggingTaskNode() + s["n2"] = GafferDispatchTest.LoggingTaskNode() + s["n3"] = GafferDispatchTest.LoggingTaskNode() + s["n2"]["preTasks"][0].setInput(s["n1"]["task"]) + + with mock.patch('GafferDeadline.DeadlineTools.submitJob', return_value=("testID", "testMessage")): + jobs = self.__job([s["n2"], s["n3"]]) + + self.assertEqual(len(jobs), 3) + dependency_count = {"n1":0, "n2": 1, "n3": 0} + self.assertEqual(len(jobs[0].getParentJobs()), dependency_count[jobs[0].getJobProperties()["Name"]]) + self.assertEqual(len(jobs[1].getParentJobs()), dependency_count[jobs[1].getJobProperties()["Name"]]) + self.assertEqual(len(jobs[2].getParentJobs()), dependency_count[jobs[2].getJobProperties()["Name"]]) + + def testSharedPreTasks(self): + # n1 + # / \ + # i1 i2 + # \ / + # n2 + + s = Gaffer.ScriptNode() + + s["n1"] = GafferDispatchTest.LoggingTaskNode() + s["i1"] = GafferDispatchTest.LoggingTaskNode() + s["i1"]["preTasks"][0].setInput(s["n1"]["task"]) + s["i2"] = GafferDispatchTest.LoggingTaskNode() + s["i2"]["preTasks"][0].setInput(s["n1"]["task"]) + s["n2"] = GafferDispatchTest.LoggingTaskNode() + s["n2"]["preTasks"][0].setInput(s["i1"]["task"]) + s["n2"]["preTasks"][1].setInput(s["i2"]["task"]) + + with mock.patch('GafferDeadline.DeadlineTools.submitJob', return_value=("testID", "testMessage")): + jobs = self.__job([s["n2"]]) + + self.assertEqual(len(jobs), 4) + self.assertEqual(len(jobs[-1].getParentJobs()), 2) + self.assertEqual(len(jobs[-1].getParentJobs()[0].getParentJobs()), 1) + self.assertEqual(len(jobs[-1].getParentJobs()[0].getParentJobs()[0].getParentJobs()), 0) + + def testNoDependencies(self): + # n1 + # | + # n2 + + s = Gaffer.ScriptNode() + + s["n1"] = GafferDispatchTest.LoggingTaskNode() + s["n1"]["frame"] = Gaffer.StringPlug(defaultValue="${frame}", flags=Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic) + s["n1"]["dispatcher"]["batchSize"].setValue(10) + + s["n2"] = GafferDispatchTest.LoggingTaskNode() + s["n2"]["frame"] = Gaffer.StringPlug(defaultValue="${frame}", flags=Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic) + s["n2"]["dispatcher"]["batchSize"].setValue(13) + s["n2"]["preTasks"][0].setInput(s["n1"]["task"]) + + dispatcher = self.__dispatcher() + dispatcher["framesMode"].setValue(dispatcher.FramesMode.CustomRange) + dispatcher["frameRange"].setValue("1-50") + + with mock.patch('GafferDeadline.DeadlineTools.submitJob', return_value=("testID", "testMessage")): + jobs = self.__job([s["n2"]], dispatcher) + + self.assertEqual(len(jobs), 2) + for j in jobs: + if j.getJobProperties()["Name"] == "n2": + self.assertEqual(len(j.getDependencies()), 8) + self.assertEqual(len(j.getTasks()), 4) + elif j.getJobProperties()["Name"] == "n1": + self.assertEqual(len(j.getDependencies()), 0) + self.assertEqual(len(j.getTasks()), 5) + self.assertEqual(j.getDependencyType(), GafferDeadline.GafferDeadlineJob.DeadlineDependencyType.none) + + def testOverrideNone(self): + # n1 + # | + # n2 + + s = Gaffer.ScriptNode() + + s["n1"] = GafferDispatchTest.LoggingTaskNode() + s["n1"]["frame"] = Gaffer.StringPlug(defaultValue="${frame}", flags=Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic) + s["n1"]["dispatcher"]["batchSize"].setValue(10) + + s["n2"] = GafferDispatchTest.LoggingTaskNode() + s["n2"]["frame"] = Gaffer.StringPlug(defaultValue="${frame}", flags=Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic) + s["n2"]["dispatcher"]["batchSize"].setValue(13) + s["n2"]["dispatcher"]["deadline"]["dependencyMode"].setValue("None") + s["n2"]["preTasks"][0].setInput(s["n1"]["task"]) + + dispatcher = self.__dispatcher() + dispatcher["framesMode"].setValue(dispatcher.FramesMode.CustomRange) + dispatcher["frameRange"].setValue("1-50") + + with mock.patch('GafferDeadline.DeadlineTools.submitJob', return_value=("testID", "testMessage")): + jobs = self.__job([s["n2"]], dispatcher) + + self.assertEqual(len(jobs), 2) + for j in jobs: + if j.getJobProperties()["Name"] == "n2": + self.assertEqual(len(j.getDependencies()), 8) + self.assertEqual(len(j.getTasks()), 4) + self.assertEqual(j.getDependencyType(), GafferDeadline.GafferDeadlineJob.DeadlineDependencyType.none) + elif j.getJobProperties()["Name"] == "n1": + self.assertEqual(len(j.getDependencies()), 0) + self.assertEqual(len(j.getTasks()), 5) + self.assertEqual(j.getDependencyType(), GafferDeadline.GafferDeadlineJob.DeadlineDependencyType.none) + + def testOverrideJob(self): + # n1 + # | + # n2 + + s = Gaffer.ScriptNode() + + s["n1"] = GafferDispatchTest.LoggingTaskNode() + s["n1"]["frame"] = Gaffer.StringPlug(defaultValue="${frame}", flags=Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic) + s["n1"]["dispatcher"]["batchSize"].setValue(10) + + s["n2"] = GafferDispatchTest.LoggingTaskNode() + s["n2"]["frame"] = Gaffer.StringPlug(defaultValue="${frame}", flags=Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic) + s["n2"]["dispatcher"]["batchSize"].setValue(13) + s["n2"]["dispatcher"]["deadline"]["dependencyMode"].setValue("Job") + s["n2"]["preTasks"][0].setInput(s["n1"]["task"]) + + dispatcher = self.__dispatcher() + dispatcher["framesMode"].setValue(dispatcher.FramesMode.CustomRange) + dispatcher["frameRange"].setValue("1-50") + + with mock.patch('GafferDeadline.DeadlineTools.submitJob', return_value=("testID", "testMessage")): + jobs = self.__job([s["n2"]], dispatcher) + + self.assertEqual(len(jobs), 2) + for j in jobs: + if j.getJobProperties()["Name"] == "n2": + self.assertEqual(len(j.getDependencies()), 8) + self.assertEqual(len(j.getTasks()), 4) + self.assertEqual(j.getDependencyType(), GafferDeadline.GafferDeadlineJob.DeadlineDependencyType.job_to_job) + elif j.getJobProperties()["Name"] == "n1": + self.assertEqual(len(j.getDependencies()), 0) + self.assertEqual(len(j.getTasks()), 5) + self.assertEqual(j.getDependencyType(), GafferDeadline.GafferDeadlineJob.DeadlineDependencyType.none) + + def testOverrideJob(self): + # n1 + # | + # n2 + + s = Gaffer.ScriptNode() + + s["n1"] = GafferDispatchTest.LoggingTaskNode() + s["n1"]["frame"] = Gaffer.StringPlug(defaultValue="${frame}", flags=Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic) + s["n1"]["dispatcher"]["batchSize"].setValue(10) + + s["n2"] = GafferDispatchTest.LoggingTaskNode() + s["n2"]["frame"] = Gaffer.StringPlug(defaultValue="${frame}", flags=Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic) + s["n2"]["dispatcher"]["batchSize"].setValue(13) + s["n2"]["dispatcher"]["deadline"]["dependencyMode"].setValue("Frame") + s["n2"]["preTasks"][0].setInput(s["n1"]["task"]) + + dispatcher = self.__dispatcher() + dispatcher["framesMode"].setValue(dispatcher.FramesMode.CustomRange) + dispatcher["frameRange"].setValue("1-50") + + with mock.patch('GafferDeadline.DeadlineTools.submitJob', return_value=("testID", "testMessage")): + jobs = self.__job([s["n2"]], dispatcher) + + self.assertEqual(len(jobs), 2) + for j in jobs: + if j.getJobProperties()["Name"] == "n2": + self.assertEqual(len(j.getDependencies()), 8) + self.assertEqual(len(j.getTasks()), 4) + self.assertEqual(j.getDependencyType(), GafferDeadline.GafferDeadlineJob.DeadlineDependencyType.frame_to_frame) + elif j.getJobProperties()["Name"] == "n1": + self.assertEqual(len(j.getDependencies()), 0) + self.assertEqual(len(j.getTasks()), 5) + self.assertEqual(j.getDependencyType(), GafferDeadline.GafferDeadlineJob.DeadlineDependencyType.none) + + def testOverrideScript(self): + # n1 + # | + # n2 + + s = Gaffer.ScriptNode() + + s["n1"] = GafferDispatchTest.LoggingTaskNode() + s["n1"]["frame"] = Gaffer.StringPlug(defaultValue="${frame}", flags=Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic) + s["n1"]["dispatcher"]["batchSize"].setValue(10) + + s["n2"] = GafferDispatchTest.LoggingTaskNode() + s["n2"]["frame"] = Gaffer.StringPlug(defaultValue="${frame}", flags=Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic) + s["n2"]["dispatcher"]["batchSize"].setValue(13) + s["n2"]["dispatcher"]["deadline"]["dependencyMode"].setValue("Script") + s["n2"]["preTasks"][0].setInput(s["n1"]["task"]) + + dispatcher = self.__dispatcher() + dispatcher["framesMode"].setValue(dispatcher.FramesMode.CustomRange) + dispatcher["frameRange"].setValue("1-50") + + with mock.patch('GafferDeadline.DeadlineTools.submitJob', return_value=("testID", "testMessage")): + jobs = self.__job([s["n2"]], dispatcher) + + self.assertEqual(len(jobs), 2) + for j in jobs: + if j.getJobProperties()["Name"] == "n2": + self.assertEqual(len(j.getDependencies()), 8) + self.assertEqual(len(j.getTasks()), 4) + self.assertEqual(j.getDependencyType(), GafferDeadline.GafferDeadlineJob.DeadlineDependencyType.scripted) + elif j.getJobProperties()["Name"] == "n1": + self.assertEqual(len(j.getDependencies()), 0) + self.assertEqual(len(j.getTasks()), 5) + self.assertEqual(j.getDependencyType(), GafferDeadline.GafferDeadlineJob.DeadlineDependencyType.none) + + def testSequence(self): + s = Gaffer.ScriptNode() + + s["n"] = GafferDispatch.PythonCommand() + s["n"]["command"] = Gaffer.StringPlug(defaultValue="print context.getFrame()", flags=Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic) + s["n"]["sequence"].setValue(True) + s["n"]["dispatcher"]["batchSize"].setValue(1) + + dispatcher = self.__dispatcher() + dispatcher["framesMode"].setValue(dispatcher.FramesMode.CustomRange) + dispatcher["frameRange"].setValue("1-50") + + with mock.patch('GafferDeadline.DeadlineTools.submitJob', return_value=("testID", "testMessage")): + jobs = self.__job([s["n"]], dispatcher) + + self.assertEqual(len(jobs), 1) + for j in jobs: + if j.getJobProperties()["Name"] == "n": + self.assertEqual(len(j.getDependencies()), 0) + self.assertEqual(len(j.getTasks()), 1) + + def testDot(self): + # n1 + # | + # d1 + # | + # n2 + + s = Gaffer.ScriptNode() + + s["n1"] = GafferDispatchTest.LoggingTaskNode() + s["n1"]["frame"] = Gaffer.StringPlug(defaultValue="${frame}", flags=Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic) + s["n1"]["dispatcher"]["batchSize"].setValue(10) + + s["n2"] = GafferDispatchTest.LoggingTaskNode() + s["n2"]["frame"] = Gaffer.StringPlug(defaultValue="${frame}", flags=Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic) + s["n2"]["dispatcher"]["batchSize"].setValue(13) + + s["d1"] = Gaffer.Dot() + s["d1"].setup(s["n2"]["preTasks"][0]) + s["d1"]["in"].setInput(s["n1"]["task"]) + s["n2"]["preTasks"][0].setInput(s["d1"]["out"]) + + dispatcher = self.__dispatcher() + dispatcher["framesMode"].setValue(dispatcher.FramesMode.CustomRange) + dispatcher["frameRange"].setValue("1-50") + + with mock.patch('GafferDeadline.DeadlineTools.submitJob', return_value=("testID", "testMessage")): + jobs = self.__job([s["n2"]], dispatcher) + + self.assertEqual(len(jobs), 2) + for j in jobs: + if j.getJobProperties()["Name"] == "n2": + self.assertEqual(len(j.getDependencies()), 8) + self.assertEqual(len(j.getTasks()), 4) + elif j.getJobProperties()["Name"] == "n1": + self.assertEqual(len(j.getDependencies()), 0) + self.assertEqual(len(j.getTasks()), 5) + self.assertEqual(j.getDependencyType(), GafferDeadline.GafferDeadlineJob.DeadlineDependencyType.none) + + def testDependencies(self): + # n1 + # / \ + # i1 i2 + # \ / + # n2 + + s = Gaffer.ScriptNode() + + s["n1"] = GafferDispatchTest.LoggingTaskNode() + s["n1"]["frame"] = Gaffer.StringPlug(defaultValue="${frame}", flags=Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic) + s["n1"]["dispatcher"]["batchSize"].setValue(10) + + s["i1"] = GafferDispatchTest.LoggingTaskNode() + s["i1"]["frame"] = Gaffer.StringPlug(defaultValue="${frame}", flags=Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic) + s["i1"]["dispatcher"]["batchSize"].setValue(25) + s["i1"]["preTasks"][0].setInput(s["n1"]["task"]) + + s["i2"] = GafferDispatchTest.LoggingTaskNode() + s["i2"]["frame"] = Gaffer.StringPlug(defaultValue="${frame}", flags=Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic) + s["i2"]["dispatcher"]["batchSize"].setValue(1) + s["i2"]["preTasks"][0].setInput(s["n1"]["task"]) + + s["n2"] = GafferDispatchTest.LoggingTaskNode() + s["n2"]["frame"] = Gaffer.StringPlug(defaultValue="${frame}", flags=Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic) + s["n2"]["dispatcher"]["batchSize"].setValue(13) + s["n2"]["preTasks"][0].setInput(s["i1"]["task"]) + s["n2"]["preTasks"][1].setInput(s["i2"]["task"]) + + dispatcher = self.__dispatcher() + dispatcher["framesMode"].setValue(dispatcher.FramesMode.CustomRange) + dispatcher["frameRange"].setValue("1-50") + + with mock.patch('GafferDeadline.DeadlineTools.submitJob', return_value=("testID", "testMessage")): + jobs = self.__job([s["n2"]], dispatcher) + + self.assertEqual(len(jobs), 4) + for j in jobs: + if j.getJobProperties()["Name"] == "n2": + self.assertEqual(len(j.getDependencies()), 55) + self.assertEqual(len(j.getTasks()), 4) + elif j.getJobProperties()["Name"] == "i2": + self.assertEqual(len(j.getDependencies()), 50) + self.assertEqual(len(j.getTasks()), 50) + elif j.getJobProperties()["Name"] == "i1": + self.assertEqual(len(j.getDependencies()), 6) + self.assertEqual(len(j.getTasks()), 2) + elif j.getJobProperties()["Name"] == "n1": + self.assertEqual(len(j.getDependencies()), 0) + self.assertEqual(len(j.getTasks()), 5) + + def testControlNodes(self): + # t = TaskList node + # n1 + # | + # n2 n3 + # \ / + # n4 t1 + # \ / + # t2 + + s = Gaffer.ScriptNode() + + s["n1"] = GafferDispatchTest.LoggingTaskNode() + s["n1"]["frame"] = Gaffer.StringPlug(defaultValue="${frame}", flags=Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic) + s["n1"]["dispatcher"]["batchSize"].setValue(10) + + s["n2"] = GafferDispatchTest.LoggingTaskNode() + s["n2"]["frame"] = Gaffer.StringPlug(defaultValue="${frame}", flags=Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic) + s["n2"]["dispatcher"]["batchSize"].setValue(25) + s["n2"]["preTasks"][0].setInput(s["n1"]["task"]) + + s["n3"] = GafferDispatchTest.LoggingTaskNode() + s["n3"]["frame"] = Gaffer.StringPlug(defaultValue="${frame}", flags=Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic) + s["n3"]["dispatcher"]["batchSize"].setValue(1) + + s["n4"] = GafferDispatchTest.LoggingTaskNode() + s["n4"]["frame"] = Gaffer.StringPlug(defaultValue="${frame}", flags=Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic) + s["n4"]["dispatcher"]["batchSize"].setValue(1) + + s["t1"] = GafferDispatch.TaskList() + s["t1"]["dispatcher"]["batchSize"].setValue(10) + s["t1"]["preTasks"][0].setInput(s["n2"]["task"]) + s["t1"]["preTasks"][1].setInput(s["n3"]["task"]) + + s["t2"] = GafferDispatch.TaskList() + s["t2"]["dispatcher"]["batchSize"].setValue(8) + s["t2"]["preTasks"][0].setInput(s["n4"]["task"]) + s["t2"]["preTasks"][1].setInput(s["t1"]["task"]) + + dispatcher = self.__dispatcher() + dispatcher["framesMode"].setValue(dispatcher.FramesMode.CustomRange) + dispatcher["frameRange"].setValue("1-50") + + with mock.patch('GafferDeadline.DeadlineTools.submitJob', return_value=("testID", "testMessage")): + jobs = self.__job([s["t2"]], dispatcher) + + self.assertEqual(len(jobs), 6) + for j in jobs: + if j.getJobProperties()["Name"] == "t2": + self.assertEqual(len(j.getDependencies()), 60) + self.assertEqual(len(j.getTasks()), 7) + if j.getJobProperties()["Name"] == "t1": + self.assertEqual(len(j.getDependencies()), 56) + self.assertEqual(len(j.getTasks()), 5) + if j.getJobProperties()["Name"] == "n4": + self.assertEqual(len(j.getDependencies()), 0) + self.assertEqual(len(j.getTasks()), 50) + if j.getJobProperties()["Name"] == "n3": + self.assertEqual(len(j.getDependencies()), 0) + self.assertEqual(len(j.getTasks()), 50) + if j.getJobProperties()["Name"] == "n2": + self.assertEqual(len(j.getDependencies()), 6) + self.assertEqual(len(j.getTasks()), 2) + if j.getJobProperties()["Name"] == "n1": + self.assertEqual(len(j.getDependencies()), 0) + self.assertEqual(len(j.getTasks()), 5) + + def testMultipleDependency(self): + # n1 + # / \ + # n2 n3 + # | | + # n4 | + # \ / + # t1 + # | + # n5 + s = Gaffer.ScriptNode() + + s["n1"] = GafferDispatchTest.LoggingTaskNode() + s["n1"]["frame"] = Gaffer.StringPlug(defaultValue="${frame}", flags=Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic) + s["n1"]["dispatcher"]["batchSize"].setValue(1) + + s["n2"] = GafferDispatchTest.LoggingTaskNode() + s["n2"]["frame"] = Gaffer.StringPlug(defaultValue="${frame}", flags=Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic) + s["n2"]["dispatcher"]["batchSize"].setValue(15) + s["n2"]["preTasks"][0].setInput(s["n1"]["task"]) + + s["n3"] = GafferDispatchTest.LoggingTaskNode() + s["n3"]["frame"] = Gaffer.StringPlug(defaultValue="${frame}", flags=Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic) + s["n3"]["dispatcher"]["batchSize"].setValue(50) + s["n3"]["preTasks"][0].setInput(s["n1"]["task"]) + + s["n4"] = GafferDispatchTest.LoggingTaskNode() + s["n4"]["frame"] = Gaffer.StringPlug(defaultValue="${frame}", flags=Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic) + s["n4"]["dispatcher"]["batchSize"].setValue(15) + s["n4"]["preTasks"][0].setInput(s["n2"]["task"]) + + s["t1"] = GafferDispatch.TaskList() + s["t1"]["dispatcher"]["batchSize"].setValue(10) + s["t1"]["preTasks"][0].setInput(s["n4"]["task"]) + s["t1"]["preTasks"][1].setInput(s["n3"]["task"]) + + + dispatcher = self.__dispatcher() + dispatcher["framesMode"].setValue(dispatcher.FramesMode.CustomRange) + dispatcher["frameRange"].setValue("1-50") + + with mock.patch('GafferDeadline.DeadlineTools.submitJob', return_value=("testID", "testMessage")): + jobs = self.__job([s["t1"]], dispatcher) + + self.assertEqual(len(jobs), 5) + for j in jobs: + if j.getJobProperties()["Name"] == "n2": + self.assertEqual(len(j.getDependencies()), 50) + self.assertEqual(len(j.getTasks()), 4) + self.assertEqual(j.getDependencyType(), GafferDeadline.GafferDeadlineJob.DeadlineDependencyType.frame_to_frame) + elif j.getJobProperties()["Name"] == "n1": + self.assertEqual(len(j.getDependencies()), 0) + self.assertEqual(len(j.getTasks()), 50) + elif j.getJobProperties()["Name"] == "n3": + self.assertEqual(len(j.getDependencies()), 50) + self.assertEqual(len(j.getTasks()), 1) + self.assertEqual(j.getDependencyType(), GafferDeadline.GafferDeadlineJob.DeadlineDependencyType.frame_to_frame) + elif j.getJobProperties()["Name"] == "n4": + self.assertEqual(len(j.getDependencies()), 4) + self.assertEqual(len(j.getTasks()), 4) + self.assertEqual(j.getDependencyType(), GafferDeadline.GafferDeadlineJob.DeadlineDependencyType.frame_to_frame) + elif j.getJobProperties()["Name"] == "t1": + self.assertEqual(len(j.getDependencies()), 12) + self.assertEqual(len(j.getTasks()), 5) + self.assertEqual(j.getDependencyType(), GafferDeadline.GafferDeadlineJob.DeadlineDependencyType.frame_to_frame) + + def testFrameMask(self): + # n1 + # | + # m1 + # | + # n2 + s = Gaffer.ScriptNode() + + s["n1"] = GafferDispatchTest.LoggingTaskNode() + s["n1"]["frame"] = Gaffer.StringPlug(defaultValue="${frame}", flags=Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic) + s["n1"]["dispatcher"]["batchSize"].setValue(1) + + s["m1"] = GafferDispatch.FrameMask() + s["m1"]["mask"].setValue("16-20") + s["m1"]["dispatcher"]["batchSize"].setValue(1) + s["m1"]["preTasks"][0].setInput(s["n1"]["task"]) + + s["n2"] = GafferDispatchTest.LoggingTaskNode() + s["n2"]["frame"] = Gaffer.StringPlug(defaultValue="${frame}", flags=Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic) + s["n2"]["dispatcher"]["batchSize"].setValue(1) + s["n2"]["preTasks"][0].setInput(s["m1"]["task"]) + + dispatcher = self.__dispatcher() + dispatcher["framesMode"].setValue(dispatcher.FramesMode.CustomRange) + dispatcher["frameRange"].setValue("1-50") + + with mock.patch('GafferDeadline.DeadlineTools.submitJob', return_value=("testID", "testMessage")): + jobs = self.__job([s["n2"]], dispatcher) + + self.assertEqual(len(jobs), 3) + for j in jobs: + if j.getJobProperties()["Name"] == "n2": + self.assertEqual(len(j.getDependencies()), 50) + self.assertEqual(len(j.getTasks()), 50) + self.assertEqual(j.getDependencyType(), GafferDeadline.GafferDeadlineJob.DeadlineDependencyType.job_to_job) + elif j.getJobProperties()["Name"] == "n1": + self.assertEqual(len(j.getDependencies()), 0) + self.assertEqual(len(j.getTasks()), 5) + + def testOffsetFrameDependency(self): + # n1 + # | + # c1 ---- e1 + # | + # n2 + s = Gaffer.ScriptNode() + + s["n1"] = GafferDispatchTest.LoggingTaskNode() + s["n1"]["frame"] = Gaffer.StringPlug(defaultValue="${frame}", flags=Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic) + s["n1"]["dispatcher"]["batchSize"].setValue(1) + + s["c1"] = GafferDispatch.TaskContextVariables() + s["c1"]["variables"].addChild( Gaffer.CompoundDataPlug.MemberPlug( "member1", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) + s["c1"]["variables"]["member1"].addChild( Gaffer.StringPlug( "name", defaultValue = '', flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) + s["c1"]["variables"]["member1"].addChild( Gaffer.FloatPlug( "value", defaultValue = 0.0, flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) + s["c1"]["variables"]["member1"].addChild( Gaffer.BoolPlug( "enabled", defaultValue = True, flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) + s["c1"]["variables"]["member1"]["name"].setValue( 'frame' ) + s["c1"]["dispatcher"]["batchSize"].setValue(1) + s["c1"]["preTasks"][0].setInput(s["n1"]["task"]) + + s["e"] = Gaffer.Expression() + s["e"].setExpression( 'parent["c1"]["variables"]["member1"]["value"] = context.getFrame() + 100', "python") + + s["n2"] = GafferDispatchTest.LoggingTaskNode() + s["n2"]["frame"] = Gaffer.StringPlug(defaultValue="${frame}", flags=Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic) + s["n2"]["dispatcher"]["batchSize"].setValue(1) + s["n2"]["preTasks"][0].setInput(s["c1"]["task"]) + + dispatcher = self.__dispatcher() + dispatcher["framesMode"].setValue(dispatcher.FramesMode.CustomRange) + dispatcher["frameRange"].setValue("1-50") + + with mock.patch('GafferDeadline.DeadlineTools.submitJob', return_value=("testID", "testMessage")): + jobs = self.__job([s["n2"]], dispatcher) + + self.assertEqual(len(jobs), 3) + for j in jobs: + if j.getJobProperties()["Name"] == "n2": + self.assertEqual(len(j.getDependencies()), 50) + self.assertEqual(len(j.getTasks()), 50) + self.assertEqual(j.getDependencyType(), GafferDeadline.GafferDeadlineJob.DeadlineDependencyType.frame_to_frame) + self.assertEqual(j._frame_dependency_offset_start, 100) + self.assertEqual(j._frame_dependency_offset_end, 100) + elif j.getJobProperties()["Name"] == "n1": + self.assertEqual(len(j.getDependencies()), 0) + self.assertEqual(len(j.getTasks()), 50) + + def testScriptFrameDependency(self): + # n1 + # | + # c1 ---- e1 + # | + # n2 + s = Gaffer.ScriptNode() + + s["n1"] = GafferDispatchTest.LoggingTaskNode() + s["n1"]["frame"] = Gaffer.StringPlug(defaultValue="${frame}", flags=Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic) + s["n1"]["dispatcher"]["batchSize"].setValue(1) + + s["c1"] = GafferDispatch.TaskContextVariables() + s["c1"]["variables"].addChild( Gaffer.CompoundDataPlug.MemberPlug( "member1", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) + s["c1"]["variables"]["member1"].addChild( Gaffer.StringPlug( "name", defaultValue = '', flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) + s["c1"]["variables"]["member1"].addChild( Gaffer.FloatPlug( "value", defaultValue = 0.0, flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) + s["c1"]["variables"]["member1"].addChild( Gaffer.BoolPlug( "enabled", defaultValue = True, flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) + s["c1"]["variables"]["member1"]["name"].setValue( 'frame' ) + s["c1"]["dispatcher"]["batchSize"].setValue(1) + s["c1"]["preTasks"][0].setInput(s["n1"]["task"]) + + s["e"] = Gaffer.Expression() + s["e"].setExpression( 'import random\nrandom.seed=(context.getFrame())\nparent["c1"]["variables"]["member1"]["value"] = random.randint(1,100000)', "python") + + s["n2"] = GafferDispatchTest.LoggingTaskNode() + s["n2"]["frame"] = Gaffer.StringPlug(defaultValue="${frame}", flags=Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic) + s["n2"]["dispatcher"]["batchSize"].setValue(1) + s["n2"]["preTasks"][0].setInput(s["c1"]["task"]) + + dispatcher = self.__dispatcher() + dispatcher["framesMode"].setValue(dispatcher.FramesMode.CustomRange) + dispatcher["frameRange"].setValue("1-50") + + with mock.patch('GafferDeadline.DeadlineTools.submitJob', return_value=("testID", "testMessage")): + jobs = self.__job([s["n2"]], dispatcher) + + self.assertEqual(len(jobs), 3) + for j in jobs: + if j.getJobProperties()["Name"] == "n2": + self.assertEqual(len(j.getDependencies()), 50) + self.assertEqual(len(j.getTasks()), 50) + self.assertEqual(j.getDependencyType(), GafferDeadline.GafferDeadlineJob.DeadlineDependencyType.scripted) + elif j.getJobProperties()["Name"] == "n1": + self.assertEqual(len(j.getDependencies()), 0) + self.assertEqual(len(j.getTasks()), 50) + + +if __name__ == "__main__": + unittest.main() diff --git a/python/GafferDeadlineTest/GafferDeadlineJobTest.py b/python/GafferDeadlineTest/GafferDeadlineJobTest.py new file mode 100644 index 0000000..f8fbcfb --- /dev/null +++ b/python/GafferDeadlineTest/GafferDeadlineJobTest.py @@ -0,0 +1,134 @@ +########################################################################## +# +# Copyright (c) 2019, Hypothetical Inc. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above +# copyright notice, this list of conditions and the following +# disclaimer. +# +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials provided with +# the distribution. +# +# * Neither the name of Hypothetical Inc. nor the names of +# any other contributors to this software may be used to endorse or +# promote products derived from this software without specific prior +# written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +########################################################################## + +import os +import unittest + +import Gaffer +import GafferTest +import GafferDeadline +import GafferDispatch + + +class GafferDeadlineJobTest(GafferTest.TestCase): + def testJobProperties(self): + dj = GafferDeadline.GafferDeadlineJob() + self.assertIn("Plugin", dj.getJobProperties()) + dj.setJobProperties({"testProp": "testVal"}) + self.assertIn("Plugin", dj.getJobProperties()) + self.assertIn("testProp", dj.getJobProperties()) + self.assertEqual(dj.getJobProperties()["testProp"], "testVal") + + dj = GafferDeadline.GafferDeadlineJob(job_properties={"testProp2": "testVal2"}) + self.assertIn("Plugin", dj.getJobProperties()) + self.assertIn("testProp2", dj.getJobProperties()) + self.assertEqual(dj.getJobProperties()["testProp2"], "testVal2") + + def testPluginProperties(self): + dj = GafferDeadline.GafferDeadlineJob() + self.assertEqual(dj.getPluginProperties(), {}) + dj.setPluginProperties({"testProp": "testVal"}) + self.assertIn("testProp", dj.getPluginProperties()) + self.assertEqual(dj.getPluginProperties()["testProp"], "testVal") + + dj = GafferDeadline.GafferDeadlineJob(plugin_properties={"testProp2": "testVal2"}) + self.assertIn("testProp2", dj.getPluginProperties()) + self.assertEqual(dj.getPluginProperties()["testProp2"], "testVal2") + + def testAuxFiles(self): + dj = GafferDeadline.GafferDeadlineJob() + self.assertEqual(dj.getAuxFiles(), []) + dj.setAuxFiles(["file1", "file2"]) + self.assertEqual(len(dj.getAuxFiles()), 2) + self.assertIn("file1", dj.getAuxFiles()) + + dj = GafferDeadline.GafferDeadlineJob(aux_files=["file3", "file4"]) + self.assertEqual(len(dj.getAuxFiles()), 2) + self.assertIn("file3", dj.getAuxFiles()) + + def testGafferNode(self): + task_node = GafferDispatch.TaskNode() + dj = GafferDeadline.GafferDeadlineJob() + dj.setGafferNode(task_node) + self.assertEqual(dj.getGafferNode(), task_node) + + dj = GafferDeadline.GafferDeadlineJob(gaffer_node=task_node) + self.assertEqual(dj.getGafferNode(), task_node) + + self.assertRaises(ValueError, dj.setGafferNode, "bad value") + + def testAddBatch(self): + dj = GafferDeadline.GafferDeadlineJob() + dj.addBatch(None, [1, 2, 3, 4, 5]) + dj.addBatch(None, [6, 7, 8, 9, 10]) + assert(dj._tasks[0].getStartFrame() == 1) + assert(dj._tasks[0].getEndFrame() == 5) + assert(dj._tasks[1].getStartFrame() == 6) + assert(dj._tasks[1].getEndFrame() == 10) + + dj = GafferDeadline.GafferDeadlineJob() + dj.addBatch(None, [1, 2, 3, 7, 8, 9, 100.0, 101.0, 102.0]) + assert(len(dj._tasks) == 3) + assert(dj._tasks[0].getTaskNumber() == 0) + assert(dj._tasks[0].getStartFrame() == 1) + assert(dj._tasks[0].getEndFrame() == 3) + assert(dj._tasks[1].getTaskNumber() == 1) + assert(dj._tasks[1].getStartFrame() == 7) + assert(dj._tasks[1].getEndFrame() == 9) + assert(dj._tasks[2].getTaskNumber() == 2) + assert(dj._tasks[2].getStartFrame() == 100) + assert(dj._tasks[2].getEndFrame() == 102) + + def testContext(self): + dj = GafferDeadline.GafferDeadlineJob() + self.assertEqual(dj.getContext(), {}) + dj.setContext({"test": "value"}) + self.assertEqual(dj.getContext(), {"test": "value"}) + + def testParentJob(self): + djc = GafferDeadline.GafferDeadlineJob() + djp = GafferDeadline.GafferDeadlineJob() + task_node = GafferDispatch.TaskNode() + task_node2 = GafferDispatch.TaskNode() + djc.addParentJob(djp) + self.assertIn(djp, djc.getParentJobs()) + djp.setGafferNode(task_node) + self.assertEqual(djc.getParentJobByGafferNode(task_node), djp) + self.assertEqual(djc.getParentJobByGafferNode(task_node2), None) + + +if __name__ == "__main__": + unittest.main() diff --git a/python/GafferDeadlineTest/GafferDeadlineTaskTest.py b/python/GafferDeadlineTest/GafferDeadlineTaskTest.py new file mode 100644 index 0000000..da7c4de --- /dev/null +++ b/python/GafferDeadlineTest/GafferDeadlineTaskTest.py @@ -0,0 +1,79 @@ +########################################################################## +# +# Copyright (c) 2019, Hypothetical Inc. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above +# copyright notice, this list of conditions and the following +# disclaimer. +# +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials provided with +# the distribution. +# +# * Neither the name of Hypothetical Inc. nor the names of +# any other contributors to this software may be used to endorse or +# promote products derived from this software without specific prior +# written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +########################################################################## + +import os +import unittest + +import Gaffer +import GafferTest +import GafferDeadline +import GafferDispatch + + +class GafferDeadlineTaskTest(GafferTest.TestCase): + def testSetFrameRange(self): + self.assertRaises(ValueError, GafferDeadline.GafferDeadlineTask, None, 1, start_frame=1, end_frame=0) + dt = GafferDeadline.GafferDeadlineTask(None, 1, start_frame=0, end_frame=1) + self.assertRaises(ValueError, dt.setFrameRange, 1, 0) + self.assertRaises(ValueError, dt.setFrameRange, 0.1, 100) + self.assertRaises(ValueError, dt.setFrameRange, 0, 100.1) + dt.setFrameRange(0, 10) + self.assertEqual(dt.getStartFrame(), 0) + self.assertEqual(dt.getEndFrame(), 10) + dt.setFrameRange(55.0, 75.0) + self.assertEqual(dt.getStartFrame(), 55) + self.assertEqual(dt.getEndFrame(), 75) + self.assertEqual(type(dt.getStartFrame()), int) + self.assertEqual(type(dt.getEndFrame()), int) + + def testSetFrameRangeFromList(self): + dt = GafferDeadline.GafferDeadlineTask(None, 1) + self.assertRaises(ValueError, dt.setFrameRangeFromList, [0, 4, 9]) + self.assertRaises(ValueError, dt.setFrameRangeFromList, [10, 9, 8]) + self.assertRaises(ValueError, dt.setFrameRangeFromList, [1.1, 2.0, 3]) + self.assertRaises(ValueError, dt.setFrameRangeFromList, [1, 2.2, 3]) + dt.setFrameRangeFromList([1, 2, 3, 4]) + self.assertEqual(dt.getStartFrame(), 1) + self.assertEqual(dt.getEndFrame(), 4) + dt.setFrameRangeFromList([12.0, 13, 14.0]) + self.assertEqual(dt.getStartFrame(), 12) + self.assertEqual(dt.getEndFrame(), 14) + self.assertEqual(type(dt.getStartFrame()), int) + self.assertEqual(type(dt.getEndFrame()), int) + + +if __name__ == "__main__": + unittest.main() diff --git a/python/GafferDeadlineTest/__init__.py b/python/GafferDeadlineTest/__init__.py new file mode 100644 index 0000000..04c4848 --- /dev/null +++ b/python/GafferDeadlineTest/__init__.py @@ -0,0 +1,43 @@ +########################################################################## +# +# Copyright (c) 2019, Hypothetical Inc. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above +# copyright notice, this list of conditions and the following +# disclaimer. +# +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials provided with +# the distribution. +# +# * Neither the name of Hypothetical Inc. nor the names of +# any other contributors to this software may be used to endorse or +# promote products derived from this software without specific prior +# written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +########################################################################## + +import unittest + +from DeadlineDispatcherTest import DeadlineDispatcherTest +from GafferDeadlineJobTest import GafferDeadlineJobTest + +if __name__ == "__main__": + unittest.main() diff --git a/python/GafferDeadlineTest/data/gafferDeadlineTest.gfr b/python/GafferDeadlineTest/data/gafferDeadlineTest.gfr new file mode 100644 index 0000000..1e06c32 --- /dev/null +++ b/python/GafferDeadlineTest/data/gafferDeadlineTest.gfr @@ -0,0 +1,163 @@ +import Gaffer +import GafferDispatch +import GafferImage +import IECore +import imath + +Gaffer.Metadata.registerNodeValue( parent, "serialiser:milestoneVersion", 0, persistent=False ) +Gaffer.Metadata.registerNodeValue( parent, "serialiser:majorVersion", 45, persistent=False ) +Gaffer.Metadata.registerNodeValue( parent, "serialiser:minorVersion", 0, persistent=False ) +Gaffer.Metadata.registerNodeValue( parent, "serialiser:patchVersion", 0, persistent=False ) + +__children = {} + +parent["variables"].addChild( Gaffer.CompoundDataPlug.MemberPlug( "imageCataloguePort", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +parent["variables"]["imageCataloguePort"].addChild( Gaffer.StringPlug( "name", defaultValue = 'image:catalogue:port', flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +parent["variables"]["imageCataloguePort"].addChild( Gaffer.IntPlug( "value", defaultValue = 0, flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +parent["variables"].addChild( Gaffer.CompoundDataPlug.MemberPlug( "projectName", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +parent["variables"]["projectName"].addChild( Gaffer.StringPlug( "name", defaultValue = 'project:name', flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +parent["variables"]["projectName"].addChild( Gaffer.StringPlug( "value", defaultValue = 'default', flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +parent["variables"].addChild( Gaffer.CompoundDataPlug.MemberPlug( "projectRootDirectory", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +parent["variables"]["projectRootDirectory"].addChild( Gaffer.StringPlug( "name", defaultValue = 'project:rootDirectory', flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +parent["variables"]["projectRootDirectory"].addChild( Gaffer.StringPlug( "value", defaultValue = '$HOME/gaffer/projects/${project:name}', flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +parent["variables"].addChild( Gaffer.CompoundDataPlug.MemberPlug( "member1", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +parent["variables"]["member1"].addChild( Gaffer.StringPlug( "name", defaultValue = '', flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +parent["variables"]["member1"].addChild( Gaffer.IntPlug( "value", defaultValue = 0, flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +parent["variables"]["member1"].addChild( Gaffer.BoolPlug( "enabled", defaultValue = True, flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["defaultFormat"] = GafferImage.FormatPlug( "defaultFormat", defaultValue = GafferImage.Format( 1920, 1080, 1.000 ), flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) +parent.addChild( __children["defaultFormat"] ) +__children["PythonCommand"] = GafferDispatch.PythonCommand( "PythonCommand" ) +parent.addChild( __children["PythonCommand"] ) +__children["PythonCommand"]["dispatcher"].addChild( Gaffer.Plug( "deadline", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["PythonCommand"]["dispatcher"].addChild( Gaffer.Plug( "deadline1", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["PythonCommand"]["dispatcher"].addChild( Gaffer.Plug( "deadline2", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["PythonCommand"]["dispatcher"].addChild( Gaffer.Plug( "deadline3", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["PythonCommand"]["dispatcher"].addChild( Gaffer.Plug( "deadline4", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["PythonCommand"]["dispatcher"].addChild( Gaffer.Plug( "deadline5", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["PythonCommand"]["dispatcher"].addChild( Gaffer.Plug( "deadline6", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["PythonCommand"].addChild( Gaffer.V2fPlug( "__uiPosition", defaultValue = imath.V2f( 0, 0 ), flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["PythonCommand1"] = GafferDispatch.PythonCommand( "PythonCommand1" ) +parent.addChild( __children["PythonCommand1"] ) +__children["PythonCommand1"]["preTasks"].addChild( GafferDispatch.TaskNode.TaskPlug( "preTask1", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["PythonCommand1"]["dispatcher"].addChild( Gaffer.Plug( "deadline", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["PythonCommand1"]["dispatcher"].addChild( Gaffer.Plug( "deadline1", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["PythonCommand1"]["dispatcher"].addChild( Gaffer.Plug( "deadline2", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["PythonCommand1"]["dispatcher"].addChild( Gaffer.Plug( "deadline3", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["PythonCommand1"]["dispatcher"].addChild( Gaffer.Plug( "deadline4", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["PythonCommand1"]["dispatcher"].addChild( Gaffer.Plug( "deadline5", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["PythonCommand1"]["dispatcher"].addChild( Gaffer.Plug( "deadline6", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["PythonCommand1"].addChild( Gaffer.V2fPlug( "__uiPosition", defaultValue = imath.V2f( 0, 0 ), flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["TaskList"] = GafferDispatch.TaskList( "TaskList" ) +parent.addChild( __children["TaskList"] ) +__children["TaskList"]["preTasks"].addChild( GafferDispatch.TaskNode.TaskPlug( "preTask1", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["TaskList"]["preTasks"].addChild( GafferDispatch.TaskNode.TaskPlug( "preTask2", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["TaskList"]["preTasks"].addChild( GafferDispatch.TaskNode.TaskPlug( "preTask3", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["TaskList"]["postTasks"].addChild( GafferDispatch.TaskNode.TaskPlug( "postTask1", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["TaskList"]["dispatcher"].addChild( Gaffer.Plug( "deadline", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["TaskList"]["dispatcher"].addChild( Gaffer.Plug( "deadline1", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["TaskList"]["dispatcher"].addChild( Gaffer.Plug( "deadline2", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["TaskList"]["dispatcher"].addChild( Gaffer.Plug( "deadline3", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["TaskList"]["dispatcher"].addChild( Gaffer.Plug( "deadline4", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["TaskList"]["dispatcher"].addChild( Gaffer.Plug( "deadline5", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["TaskList"]["dispatcher"].addChild( Gaffer.Plug( "deadline6", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["TaskList"].addChild( Gaffer.V2fPlug( "__uiPosition", defaultValue = imath.V2f( 0, 0 ), flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["PythonCommand2"] = GafferDispatch.PythonCommand( "PythonCommand2" ) +parent.addChild( __children["PythonCommand2"] ) +__children["PythonCommand2"]["dispatcher"].addChild( Gaffer.Plug( "deadline", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["PythonCommand2"]["dispatcher"].addChild( Gaffer.Plug( "deadline1", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["PythonCommand2"]["dispatcher"].addChild( Gaffer.Plug( "deadline2", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["PythonCommand2"]["dispatcher"].addChild( Gaffer.Plug( "deadline3", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["PythonCommand2"]["dispatcher"].addChild( Gaffer.Plug( "deadline4", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["PythonCommand2"]["dispatcher"].addChild( Gaffer.Plug( "deadline5", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["PythonCommand2"]["dispatcher"].addChild( Gaffer.Plug( "deadline6", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["PythonCommand2"].addChild( Gaffer.V2fPlug( "__uiPosition", defaultValue = imath.V2f( 0, 0 ), flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["PythonCommand3"] = GafferDispatch.PythonCommand( "PythonCommand3" ) +parent.addChild( __children["PythonCommand3"] ) +__children["PythonCommand3"]["dispatcher"].addChild( Gaffer.Plug( "deadline", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["PythonCommand3"]["dispatcher"].addChild( Gaffer.Plug( "deadline1", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["PythonCommand3"]["dispatcher"].addChild( Gaffer.Plug( "deadline2", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["PythonCommand3"]["dispatcher"].addChild( Gaffer.Plug( "deadline3", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["PythonCommand3"]["dispatcher"].addChild( Gaffer.Plug( "deadline4", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["PythonCommand3"]["dispatcher"].addChild( Gaffer.Plug( "deadline5", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["PythonCommand3"]["dispatcher"].addChild( Gaffer.Plug( "deadline6", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["PythonCommand3"].addChild( Gaffer.V2fPlug( "__uiPosition", defaultValue = imath.V2f( 0, 0 ), flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["TaskContextVariables"] = GafferDispatch.TaskContextVariables( "TaskContextVariables" ) +parent.addChild( __children["TaskContextVariables"] ) +__children["TaskContextVariables"]["preTasks"].addChild( GafferDispatch.TaskNode.TaskPlug( "preTask1", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["TaskContextVariables"]["dispatcher"].addChild( Gaffer.Plug( "deadline", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["TaskContextVariables"]["dispatcher"].addChild( Gaffer.Plug( "deadline1", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["TaskContextVariables"]["variables"].addChild( Gaffer.CompoundDataPlug.MemberPlug( "member1", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["TaskContextVariables"]["variables"]["member1"].addChild( Gaffer.StringPlug( "name", defaultValue = '', flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["TaskContextVariables"]["variables"]["member1"].addChild( Gaffer.StringPlug( "value", defaultValue = '', flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["TaskContextVariables"]["variables"]["member1"].addChild( Gaffer.BoolPlug( "enabled", defaultValue = True, flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["TaskContextVariables"].addChild( Gaffer.V2fPlug( "__uiPosition", defaultValue = imath.V2f( 0, 0 ), flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["TaskContextVariables1"] = GafferDispatch.TaskContextVariables( "TaskContextVariables1" ) +parent.addChild( __children["TaskContextVariables1"] ) +__children["TaskContextVariables1"]["preTasks"].addChild( GafferDispatch.TaskNode.TaskPlug( "preTask1", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["TaskContextVariables1"]["dispatcher"].addChild( Gaffer.Plug( "deadline", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["TaskContextVariables1"]["dispatcher"].addChild( Gaffer.Plug( "deadline1", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["TaskContextVariables1"]["dispatcher"].addChild( Gaffer.Plug( "deadline2", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["TaskContextVariables1"]["variables"].addChild( Gaffer.CompoundDataPlug.MemberPlug( "member1", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["TaskContextVariables1"]["variables"]["member1"].addChild( Gaffer.StringPlug( "name", defaultValue = '', flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["TaskContextVariables1"]["variables"]["member1"].addChild( Gaffer.StringPlug( "value", defaultValue = '', flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["TaskContextVariables1"]["variables"]["member1"].addChild( Gaffer.BoolPlug( "enabled", defaultValue = True, flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["TaskContextVariables1"].addChild( Gaffer.V2fPlug( "__uiPosition", defaultValue = imath.V2f( 0, 0 ), flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["FrameMask"] = GafferDispatch.FrameMask( "FrameMask" ) +parent.addChild( __children["FrameMask"] ) +__children["FrameMask"]["preTasks"].addChild( GafferDispatch.TaskNode.TaskPlug( "preTask1", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["FrameMask"]["dispatcher"].addChild( Gaffer.Plug( "deadline", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["FrameMask"]["dispatcher"].addChild( Gaffer.Plug( "deadline1", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["FrameMask"].addChild( Gaffer.V2fPlug( "__uiPosition", defaultValue = imath.V2f( 0, 0 ), flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +parent["variables"]["imageCataloguePort"]["value"].setValue( 63850 ) +parent["variables"]["member1"]["name"].setValue( 'changeset' ) +parent["variables"]["member1"]["value"].setValue( 49 ) +Gaffer.Metadata.registerValue( parent["variables"]["imageCataloguePort"], 'readOnly', True ) +Gaffer.Metadata.registerValue( parent["variables"]["projectName"]["name"], 'readOnly', True ) +Gaffer.Metadata.registerValue( parent["variables"]["projectRootDirectory"]["name"], 'readOnly', True ) +__children["PythonCommand"]["dispatcher"]["batchSize"].setValue( 10 ) +__children["PythonCommand"]["dispatcher"]["deadline"]["group"].setValue( 'workstation' ) +__children["PythonCommand"]["dispatcher"]["deadline"]["onJobComplete"].setValue( 'Nothing' ) +__children["PythonCommand"]["command"].setValue( 'print "task1 {}".format(context["frame"])' ) +__children["PythonCommand"]["__uiPosition"].setValue( imath.V2f( -9.13885307, -11.8646297 ) ) +__children["PythonCommand1"]["preTasks"]["preTask0"].setInput( __children["PythonCommand"]["task"] ) +__children["PythonCommand1"]["dispatcher"]["batchSize"].setValue( 5 ) +__children["PythonCommand1"]["dispatcher"]["deadline"]["group"].setValue( 'workstation' ) +__children["PythonCommand1"]["dispatcher"]["deadline"]["onJobComplete"].setValue( 'Nothing' ) +__children["PythonCommand1"]["command"].setValue( 'print "task2 {}".format(context["frame"])' ) +__children["PythonCommand1"]["__uiPosition"].setValue( imath.V2f( -8.44070244, -20.0287018 ) ) +__children["TaskList"]["preTasks"]["preTask0"].setInput( __children["PythonCommand1"]["task"] ) +__children["TaskList"]["preTasks"]["preTask1"].setInput( __children["FrameMask"]["task"] ) +__children["TaskList"]["preTasks"]["preTask2"].setInput( __children["TaskContextVariables1"]["task"] ) +__children["TaskList"]["postTasks"]["postTask0"].setInput( __children["PythonCommand3"]["task"] ) +__children["TaskList"]["dispatcher"]["deadline"]["group"].setValue( 'workstation' ) +__children["TaskList"]["dispatcher"]["deadline"]["onJobComplete"].setValue( 'Nothing' ) +__children["TaskList"]["__uiPosition"].setValue( imath.V2f( 8.68742371, -49.6018524 ) ) +__children["PythonCommand2"]["dispatcher"]["batchSize"].setValue( 10 ) +__children["PythonCommand2"]["dispatcher"]["deadline"]["group"].setValue( 'workstation' ) +__children["PythonCommand2"]["dispatcher"]["deadline"]["limits"].setValue( 'vray' ) +__children["PythonCommand2"]["dispatcher"]["deadline"]["onJobComplete"].setValue( 'Archive' ) +__children["PythonCommand2"]["command"].setValue( 'print "task3 {}: {}".format(context["frame"], context["variation"])\n' ) +__children["PythonCommand2"]["__uiPosition"].setValue( imath.V2f( 25.0260391, -3.909657 ) ) +__children["PythonCommand3"]["dispatcher"]["deadline"]["group"].setValue( 'workstation' ) +__children["PythonCommand3"]["dispatcher"]["deadline"]["onJobComplete"].setValue( 'Nothing' ) +__children["PythonCommand3"]["command"].setValue( 'print "frame: {}".format(context["frame"])' ) +__children["PythonCommand3"]["__uiPosition"].setValue( imath.V2f( 44.4874077, -34.3202438 ) ) +__children["TaskContextVariables"]["preTasks"]["preTask0"].setInput( __children["PythonCommand2"]["task"] ) +__children["TaskContextVariables"]["dispatcher"]["batchSize"].setValue( 2 ) +__children["TaskContextVariables"]["dispatcher"]["deadline"]["onJobComplete"].setValue( 'Nothing' ) +__children["TaskContextVariables"]["variables"]["member1"]["name"].setValue( 'variation' ) +__children["TaskContextVariables"]["variables"]["member1"]["value"].setValue( 'blue' ) +__children["TaskContextVariables"]["__uiPosition"].setValue( imath.V2f( 12.5253887, -17.8737144 ) ) +__children["TaskContextVariables1"]["preTasks"]["preTask0"].setInput( __children["PythonCommand2"]["task"] ) +__children["TaskContextVariables1"]["dispatcher"]["deadline"]["onJobComplete"].setValue( 'Nothing' ) +__children["TaskContextVariables1"]["variables"]["member1"]["name"].setValue( 'variation' ) +__children["TaskContextVariables1"]["variables"]["member1"]["value"].setValue( 'red' ) +__children["TaskContextVariables1"]["__uiPosition"].setValue( imath.V2f( 31.5277596, -17.9557514 ) ) +__children["FrameMask"]["preTasks"]["preTask0"].setInput( __children["TaskContextVariables"]["task"] ) +__children["FrameMask"]["dispatcher"]["deadline"]["onJobComplete"].setValue( 'Nothing' ) +__children["FrameMask"]["mask"].setValue( '1-15' ) +__children["FrameMask"]["__uiPosition"].setValue( imath.V2f( 12.5253887, -28.0377827 ) ) + + +del __children + diff --git a/python/GafferDeadlineUI/DeadlineDispatcherUI.py b/python/GafferDeadlineUI/DeadlineDispatcherUI.py new file mode 100644 index 0000000..eb508f6 --- /dev/null +++ b/python/GafferDeadlineUI/DeadlineDispatcherUI.py @@ -0,0 +1,224 @@ +########################################################################## +# +# Copyright (c) 2019, Hypothetical Inc. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above +# copyright notice, this list of conditions and the following +# disclaimer. +# +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials provided with +# the distribution. +# +# * Neither the name of Hypothetical Inc. nor the names of +# any other contributors to this software may be used to endorse or +# promote products derived from this software without specific prior +# written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +########################################################################## + +# TODO: figure out how to get the secondary pool list from Deadline API + + +import Gaffer +import GafferDispatch +import GafferDeadline + +Gaffer.Metadata.registerNode( + + GafferDeadline.DeadlineDispatcher, + + "description", + """ + Dispatches tasks to Deadline. + """ + +) + +Gaffer.Metadata.registerNode( + + GafferDispatch.TaskNode, + + plugs={ + + "dispatcher.deadline": [ + + "description", + """ + Settings that control how tasks are + dispatched to Deadline. + """, + "layout:section", "Deadline", + "plugValueWidget:type", "GafferUI.LayoutPlugValueWidget", + + ], + + "dispatcher.deadline.comment": [ + "description", + """ + A simple description of your job. This is optional and can be left blank. + """, + ], + + "dispatcher.deadline.department": [ + "description", + """ + The department you belong to. This is optional and can be left blank. + """, + ], + + "dispatcher.deadline.pool": [ + "description", + """ + The pool that the job will be submitted to. + """, + "plugValueWidget:type", "GafferDeadlineUI.DeadlineListPlugValueWidget", + "deadlineListPlugValueWidget:type", "pools", + "deadlineListPlugValueWidget:multiSelect", False, + "userDefault", "none", + ], + + "dispatcher.deadline.secondaryPool": [ + "description", + """ + The secondary pool that the job will be submitted to. + """, + "plugValueWidget:type", "GafferDeadlineUI.DeadlineListPlugValueWidget", + "deadlineListPlugValueWidget:type", "pools", + "deadlineListPlugValueWidget:multiSelect", False, + "userDefault", "none", + ], + + "dispatcher.deadline.group": [ + "description", + """ + The group that your job will be submitted to. + """, + "plugValueWidget:type", "GafferDeadlineUI.DeadlineListPlugValueWidget", + "deadlineListPlugValueWidget:type", "groups", + "deadlineListPlugValueWidget:multiSelect", False, + "userDefault", "none", + ], + + "dispatcher.deadline.priority": [ + "description", + """ + A job can have a numeric priority ranging from 0 to 100, where 0 is the lowest priority and 100 is the highest. + """, + ], + "dispatcher.deadline.taskTimeout": [ + "description", + """ + The number of minutes a slave has to render a task for this job before it requeues it. Specify 0 for no timeout. + """, + ], + "dispatcher.deadline.enableAutoTimeout": [ + "description", + """ + If the Auto Task Timeout is properly configured in the repository options then enabling this will allow a task timeout + to be automatically calculated based on render times for previous frames of the job. + """, + ], + "dispatcher.deadline.concurrentTasks": [ + "description", + """ + The number of tasks that can render concurrently on a single Slave. This is useful if the rendering application only + uses one thread to render and your Slaves have multiple CPUs. + """, + ], + "dispatcher.deadline.limitToSlaveLimit": [ + "description", + """ + If you limit the tasks to a Slave's task limit, then by default, the Slave won't dequeue more tasks then it has CPUs. + This task limit can be overridden for individual Slaves by an administrator. + """, + ], + "dispatcher.deadline.machineLimit": [ + "description", + """ + Use the Machine Limit to specify the maximum number of machines that can render your job at one time. + Specify 0 for no limit. + """, + ], + "dispatcher.deadline.machineList": [ + "description", + """ + The whitelisted or blacklisted list of machines. + """, + "plugValueWidget:type", "GafferDeadlineUI.DeadlineListPlugValueWidget", + "deadlineListPlugValueWidget:type", "slaves", + "deadlineListPlugValueWidget:multiSelect", True + ], + "dispatcher.deadline.isBlackList": [ + "description", + """ + You can force the job to render on specific machines by using a whitelist, + or you can avoid specific machines by using a blacklist. + """, + ], + "dispatcher.deadline.limits": [ + "description", + """ + The Limits that your job requires. + """, + "plugValueWidget:type", "GafferDeadlineUI.DeadlineListPlugValueWidget", + "deadlineListPlugValueWidget:type", "limits", + "deadlineListPlugValueWidget:multiSelect", True + ], + "dispatcher.deadline.onJobComplete": [ + "description", + """ + If desired, you can automatically archive or delete the job when it completes. + """, + "preset:Nothing", "Nothing", + "preset:Archive", "Archive", + "preset:Delete", "Delete", + + "userDefault", "Nothing", + + "plugValueWidget:type", "GafferUI.PresetsPlugValueWidget", + ], + "dispatcher.deadline.submitSuspended": [ + "description", + """ + If enabled, the job will submit in the suspended state. This is useful if you + don't want the job to start rendering right away. Just resume it from the Monitor + when you want it to render. + """, + ], + "dispatcher.deadline.dependencyMode": [ + "description", + """ + Determine how downstream nodes that depend on this node will be handled. If set to auto, + the dispatcher will attempt to determine the best mode, falling back to scripted dependency checking. + """, + "preset:Auto", "Auto", + "preset:Full Job", "Job", + "preset:Per Frame", "Frame", + "preset:Scripted", "Script", + "preset:Scripted", "None" + + "userDefault", "Auto", + + "plugValueWidget:type", "GafferUI.PresetsPlugValueWidget", + ], + } + +) diff --git a/python/GafferDeadlineUI/DeadlineListPlugValueWidget.py b/python/GafferDeadlineUI/DeadlineListPlugValueWidget.py new file mode 100644 index 0000000..6c731d6 --- /dev/null +++ b/python/GafferDeadlineUI/DeadlineListPlugValueWidget.py @@ -0,0 +1,133 @@ +########################################################################## +# +# Copyright (c) 2019, Hypothetical Inc. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above +# copyright notice, this list of conditions and the following +# disclaimer. +# +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials provided with +# the distribution. +# +# * Neither the name of Hypothetical Inc. nor the names of +# any other contributors to this software may be used to endorse or +# promote products derived from this software without specific prior +# written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +########################################################################## + +import Gaffer +import GafferUI +import GafferDeadlineUI +import IECore + +from GafferDeadline import DeadlineTools + + +class DeadlineListPlugValueWidget(GafferUI.PlugValueWidget): + + def __init__(self, plug, listString="", **kw): + assert(type(listString) == str) + + self.__row = GafferUI.ListContainer(GafferUI.ListContainer.Orientation.Horizontal, spacing=4) + GafferUI.PlugValueWidget.__init__(self, self.__row, plug, **kw) + + self.__listString = listString + + listWidget = GafferUI.TextWidget(self.__listString) + self._addPopupMenu(listWidget) + self.__row.append(listWidget) + + button = GafferUI.Button("...", hasFrame=True) + self.__buttonClickedConnection = button.clickedSignal().connect(Gaffer.WeakMethod(self.__buttonClicked)) + self.__row.append(button) + + self.__editingFinishedConnection = listWidget.editingFinishedSignal().connect(Gaffer.WeakMethod(self.__setPlugValue)) + + self._updateFromPlug() + + def listWidget(self): + return self.__row[0] + + def __buttonClicked(self, widget): + # Get info from the farm. + # Keep this in the button code to prevent it from being called repeatedly by Gaffer with + # every node with a Deadline submitter attached + multiSelect = Gaffer.Metadata.value(self.getPlug(), "deadlineListPlugValueWidget:multiSelect") + if Gaffer.Metadata.value(self.getPlug(), "deadlineListPlugValueWidget:type") == "pools": + deadlineListString = ",".join(DeadlineTools.getPools()) + dialogTitle = "Select Pools" + elif Gaffer.Metadata.value(self.getPlug(), "deadlineListPlugValueWidget:type") == "groups": + deadlineListString = ",".join(DeadlineTools.getGroups()) + dialogTitle = "Select Groups" + elif Gaffer.Metadata.value(self.getPlug(), "deadlineListPlugValueWidget:type") == "slaves": + deadlineListString = ",".join(DeadlineTools.getMachineList()) + dialogTitle = "Select Slaves" + elif Gaffer.Metadata.value(self.getPlug(), "deadlineListPlugValueWidget:type") == "limits": + deadlineListString = ",".join(DeadlineTools.getLimitGroups()) + dialogTitle = "Select Limits" + + optionListString = deadlineListString.split(",") + selectionString = self.getPlug().getValue().split(",") + dialogue = GafferDeadlineUI.ListSelectionDialog(optionListString, selectionString, dialogTitle, allowMultipleSelection=multiSelect) + listString = dialogue.waitForSelection(parentWindow=self.ancestor(GafferUI.Window)) + + if listString is not None: + self.__listString = listString + self.__setPlugValue() + + def _updateFromPlug(self): + with self.getContext(): + with IECore.IgnoredExceptions(ValueError): + self.__listString = self.getPlug().getValue() + assert(type(self.__listString) == str) + + self.listWidget().setEditable(self._editable()) + self.__row[1].setEnabled(self._editable()) # button + self.listWidget().setText(self.__listString) + + def _setPlugFromString(self, listString): + self.getPlug().setValue(listString) + self._updateFromPlug() + + def setHighlighted(self, highlighted): + GafferUI.PlugValueWidget.setHighlighted(self, highlighted) + + def setReadOnly(self, readOnly): + if readOnly == self.getReadOnly(): + return + + GafferUI.PlugValueWidget.setReadOnly(self, readOnly) + + def __setPlugValue(self, *args): + if not self._editable(): + return + + with Gaffer.UndoScope(self.getPlug().ancestor(Gaffer.ScriptNode)): + if args: + self._setPlugFromString(args[0].getText()) + else: + self._setPlugFromString(self.__listString) + + # now we've transferred the text changes to the global undo queue, we remove them + # from the widget's private text editing undo queue. it will then ignore undo shortcuts, + # allowing them to fall through to the global undo shortcut. + self.listWidget().clearUndo() diff --git a/python/GafferDeadlineUI/ListSelectionDialog.py b/python/GafferDeadlineUI/ListSelectionDialog.py new file mode 100644 index 0000000..a55d167 --- /dev/null +++ b/python/GafferDeadlineUI/ListSelectionDialog.py @@ -0,0 +1,74 @@ +########################################################################## +# +# Copyright (c) 2019, Hypothetical Inc. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above +# copyright notice, this list of conditions and the following +# disclaimer. +# +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials provided with +# the distribution. +# +# * Neither the name of Hypothetical Inc. nor the names of +# any other contributors to this software may be used to endorse or +# promote products derived from this software without specific prior +# written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +########################################################################## + +import Gaffer +import GafferUI +import GafferDeadlineUI + + +class ListSelectionDialog(GafferUI.Dialogue): + def __init__(self, masterList, selectionList, title="Choose Wisely", cancelLabel="Cancel", confirmLabel="OK", allowMultipleSelection=False, **kw): + GafferUI.Dialogue.__init__(self, title, **kw) + self.__masterList = masterList + self.__selectionList = [i for i in selectionList if i in masterList] + + self.__masterListWidget = GafferDeadlineUI.ListWidget(allowMultipleSelection=allowMultipleSelection) + for i in self.__masterList: + self.__masterListWidget.addItem(i) + self.__masterListWidget.setSelectedStrings(self.__selectionList) + + self._setWidget(self.__masterListWidget) + + self.__cancelButton = self._addButton(cancelLabel) + self.__cancelButtonConnection = self.__cancelButton.clickedSignal().connect(Gaffer.WeakMethod(self.__buttonClicked)) + self.__confirmButton = self._addButton(confirmLabel) + self.__confirmButtonConnection = self.__confirmButton.clickedSignal().connect(Gaffer.WeakMethod(self.__buttonClicked)) + + def waitForSelection(self, **kw): + button = self.waitForButton(**kw) + + if button is self.__confirmButton: + return self.__result() + + return None + + def __result(self): + stringList = self.__masterListWidget.getSelectedStrings() + return ",".join(stringList) + + def __buttonClicked(self, button): + if button is self.__confirmButton: + pass diff --git a/python/GafferDeadlineUI/ListWidget.py b/python/GafferDeadlineUI/ListWidget.py new file mode 100644 index 0000000..34e4404 --- /dev/null +++ b/python/GafferDeadlineUI/ListWidget.py @@ -0,0 +1,107 @@ +########################################################################## +# +# Copyright (c) 2019, Hypothetical Inc. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above +# copyright notice, this list of conditions and the following +# disclaimer. +# +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials provided with +# the distribution. +# +# * Neither the name of Hypothetical Inc. nor the names of +# any other contributors to this software may be used to endorse or +# promote products derived from this software without specific prior +# written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +########################################################################## + +import warnings + +import IECore + +import Gaffer +import GafferUI + +import Qt +from Qt import QtCore +from Qt import QtGui +from Qt import QtWidgets + + +class ListWidget(GafferUI.Widget): + + def __init__( + self, + allowMultipleSelection=False, + **kw + ): + + GafferUI.Widget.__init__(self, QtWidgets.QListWidget(), **kw) + + self._qtWidget().setAlternatingRowColors(True) + self._qtWidget().setUniformItemSizes(True) + self._qtWidget().setResizeMode(QtWidgets.QListView.Adjust) + if allowMultipleSelection: + self._qtWidget().setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) + + self.__selectionChangedSlot = Gaffer.WeakMethod(self.__selectionChanged) + self._qtWidget().selectionModel().selectionChanged.connect(self.__selectionChangedSlot) + self.__selectionChangedSignal = GafferUI.WidgetSignal() + + def addItem(self, newString): + self._qtWidget().addItem(newString) + + def reset(self): + while self._qtWidget().count() > 0: + self._qtWidget().takeItem(1) + + # Returns a list of all currently selected strings. Note that a list is returned + # even when in single selection mode. + def getSelectedStrings(self): + return [s.text() for s in self._qtWidget().selectedItems()] + + # Sets the currently selected strings. Strings which are not currently being displayed + # will be discarded, such that subsequent calls to getSelectedStrings will not include them. + def setSelectedStrings(self, strings, scrollToFirst=True): + if self._qtWidget().selectionMode() != QtWidgets.QAbstractItemView.ExtendedSelection: + assert(len(strings) <= 1) + + selectionModel = self._qtWidget().selectionModel() + selectionModel.selectionChanged.disconnect(self.__selectionChangedSlot) + + selectionModel.clear() + + for string in strings: + matchingItems = self._qtWidget().findItems(string, QtCore.Qt.MatchExactly) + for item in matchingItems: + item.setSelected(True) + + selectionModel.selectionChanged.connect(self.__selectionChangedSlot) + + def selectionChangedSignal(self): + + return self.__selectionChangedSignal + + def __selectionChanged(self, selected, deselected): + + self.selectionChangedSignal()(self) + return True diff --git a/python/GafferDeadlineUI/__init__.py b/python/GafferDeadlineUI/__init__.py new file mode 100644 index 0000000..fe6202b --- /dev/null +++ b/python/GafferDeadlineUI/__init__.py @@ -0,0 +1,42 @@ +########################################################################## +# +# Copyright (c) 2019, Hypothetical Inc. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above +# copyright notice, this list of conditions and the following +# disclaimer. +# +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials provided with +# the distribution. +# +# * Neither the name of Hypothetical Inc. nor the names of +# any other contributors to this software may be used to endorse or +# promote products derived from this software without specific prior +# written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +########################################################################## + +import DeadlineDispatcherUI +from ListWidget import ListWidget +from DeadlineListPlugValueWidget import DeadlineListPlugValueWidget +from ListSelectionDialog import ListSelectionDialog + +__import__("IECore").loadConfig("GAFFER_STARTUP_PATHS", {}, subdirectory="GafferDeadlineUI") diff --git a/python/GafferDeadlineUITest/DocumentationTest.py b/python/GafferDeadlineUITest/DocumentationTest.py new file mode 100644 index 0000000..978b8b3 --- /dev/null +++ b/python/GafferDeadlineUITest/DocumentationTest.py @@ -0,0 +1,55 @@ +########################################################################## +# +# Copyright (c) 2016, Image Engine Design Inc. All rights reserved. +# Copyright (c) 2019, Hypothetical Inc. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above +# copyright notice, this list of conditions and the following +# disclaimer. +# +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials provided with +# the distribution. +# +# * Neither the name of Hypothetical Inc. nor the names of +# any other contributors to this software may be used to endorse or +# promote products derived from this software without specific prior +# written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +########################################################################## + +import GafferUITest +import GafferDispatch +import GafferDeadline +import GafferDeadlineUI + + +class DocumentationTest(GafferUITest.TestCase): + + def test(self): + + self.maxDiff = None + self.assertNodesAreDocumented(GafferDeadline) + # Also test GafferDispatch because we add plugs to + # TaskNodes. + self.assertNodesAreDocumented(GafferDispatch) + +if __name__ == "__main__": + unittest.main() diff --git a/python/GafferDeadlineUITest/__init__.py b/python/GafferDeadlineUITest/__init__.py new file mode 100644 index 0000000..ea8998e --- /dev/null +++ b/python/GafferDeadlineUITest/__init__.py @@ -0,0 +1,40 @@ +########################################################################## +# +# Copyright (c) 2019, Hypothetical Inc. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above +# copyright notice, this list of conditions and the following +# disclaimer. +# +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials provided with +# the distribution. +# +# * Neither the name of Hypothetical Inc. nor the names of +# any other contributors to this software may be used to endorse or +# promote products derived from this software without specific prior +# written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +########################################################################## + +from DocumentationTest import DocumentationTest + +if __name__ == "__main__": + unittest.main() diff --git a/startup/dispatch/menus.py b/startup/dispatch/menus.py new file mode 100644 index 0000000..1f0bd77 --- /dev/null +++ b/startup/dispatch/menus.py @@ -0,0 +1 @@ +import GafferDeadlineUI diff --git a/startup/dispatch/project.py b/startup/dispatch/project.py new file mode 100644 index 0000000..a19b92d --- /dev/null +++ b/startup/dispatch/project.py @@ -0,0 +1,11 @@ +import IECore +import Gaffer + +dispatchers = [] +import GafferDeadline +dispatchers.append(GafferDeadline.DeadlineDispatcher) + +for dispatcher in dispatchers: + Gaffer.Metadata.registerPlugValue(dispatcher, "jobName", "userDefault", "${script:name}") + directoryName = dispatcher.staticTypeName().rpartition(":")[2].replace("Dispatcher", "").lower() + Gaffer.Metadata.registerPlugValue(dispatcher, "jobsDirectory", "userDefault", "${project:rootDirectory}/dispatcher/" + directoryName) diff --git a/startup/execute/project.py b/startup/execute/project.py new file mode 100644 index 0000000..a19b92d --- /dev/null +++ b/startup/execute/project.py @@ -0,0 +1,11 @@ +import IECore +import Gaffer + +dispatchers = [] +import GafferDeadline +dispatchers.append(GafferDeadline.DeadlineDispatcher) + +for dispatcher in dispatchers: + Gaffer.Metadata.registerPlugValue(dispatcher, "jobName", "userDefault", "${script:name}") + directoryName = dispatcher.staticTypeName().rpartition(":")[2].replace("Dispatcher", "").lower() + Gaffer.Metadata.registerPlugValue(dispatcher, "jobsDirectory", "userDefault", "${project:rootDirectory}/dispatcher/" + directoryName) diff --git a/startup/gui/menus.py b/startup/gui/menus.py new file mode 100644 index 0000000..1f0bd77 --- /dev/null +++ b/startup/gui/menus.py @@ -0,0 +1 @@ +import GafferDeadlineUI diff --git a/startup/gui/project.py b/startup/gui/project.py new file mode 100644 index 0000000..a19b92d --- /dev/null +++ b/startup/gui/project.py @@ -0,0 +1,11 @@ +import IECore +import Gaffer + +dispatchers = [] +import GafferDeadline +dispatchers.append(GafferDeadline.DeadlineDispatcher) + +for dispatcher in dispatchers: + Gaffer.Metadata.registerPlugValue(dispatcher, "jobName", "userDefault", "${script:name}") + directoryName = dispatcher.staticTypeName().rpartition(":")[2].replace("Dispatcher", "").lower() + Gaffer.Metadata.registerPlugValue(dispatcher, "jobsDirectory", "userDefault", "${project:rootDirectory}/dispatcher/" + directoryName)