Skip to content

Commit

Permalink
Merge pull request #5568 from johnhaddon/tractorTesting
Browse files Browse the repository at this point in the history
GafferTractorTest : Test using mock API if Tractor unavailable
  • Loading branch information
johnhaddon authored Nov 28, 2023
2 parents ae1eafa + f7bf8f7 commit d1d6014
Show file tree
Hide file tree
Showing 10 changed files with 130 additions and 26 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ jobs:
publish: true
containerImage:
testRunner: Invoke-Expression
testArguments: -excludedCategories performance GafferTest GafferVDBTest GafferUSDTest GafferSceneTest GafferDispatchTest GafferOSLTest GafferImageTest GafferUITest GafferImageUITest GafferSceneUITest GafferDispatchUITest GafferOSLUITest GafferUSDUITest GafferVDBUITest GafferDelightUITest
testArguments: -excludedCategories performance GafferTest GafferVDBTest GafferUSDTest GafferSceneTest GafferDispatchTest GafferOSLTest GafferImageTest GafferUITest GafferImageUITest GafferSceneUITest GafferDispatchUITest GafferOSLUITest GafferUSDUITest GafferVDBUITest GafferDelightUITest GafferTractorTest GafferTractorUITest
sconsCacheMegabytes: 400

runs-on: ${{ matrix.os }}
Expand Down
6 changes: 6 additions & 0 deletions Changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ Improvements
- TaskList, FrameMask : Reimplemented in C++ for improved performance.
- Cache : Increased default computation cache size to 8Gb. Call `Gaffer.ValuePlug.setCacheMemoryLimit()` from a startup file to override this.

API
---

- GafferTractor : Added `tractorAPI()` method used for accessing the `tractor.api.author` module.
- GafferTractorTest : Added `tractorAPI()` method which returns a mock API if Tractor is not available. This allows the GafferTractor module to be tested without Tractor being installed.

Breaking Changes
----------------

Expand Down
11 changes: 6 additions & 5 deletions python/GafferTractor/TractorDispatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,19 +36,20 @@

import os

import tractor.api.author as author

import IECore

import Gaffer
import GafferDispatch
import GafferTractor

class TractorDispatcher( GafferDispatch.Dispatcher ) :

def __init__( self, name = "TractorDispatcher" ) :

GafferDispatch.Dispatcher.__init__( self, name )

self.__tractorAPI = GafferTractor.tractorAPI()

self["service"] = Gaffer.StringPlug( defaultValue = '"*"' )
self["envKey"] = Gaffer.StringPlug()

Expand Down Expand Up @@ -82,7 +83,7 @@ def _doDispatch( self, rootBatch ) :

context = Gaffer.Context.current()

job = author.Job(
job = self.__tractorAPI.Job(
## \todo Remove these manual substitutions once #887 is resolved.
title = context.substitute( self["jobName"].getValue() ) or "untitled",
service = context.substitute( self["service"].getValue() ),
Expand Down Expand Up @@ -143,7 +144,7 @@ def __acquireTask( self, batch, dispatchData ) :
# Make a task.

nodeName = batch.node().relativeName( dispatchData["scriptNode"] )
task = author.Task( title = nodeName )
task = self.__tractorAPI.Task( title = nodeName )

if batch.frames() :

Expand Down Expand Up @@ -172,7 +173,7 @@ def __acquireTask( self, batch, dispatchData ) :
# Create a Tractor command to execute that command line, and add
# it to the task.

command = author.Command( argv = args )
command = self.__tractorAPI.Command( argv = args )
task.addCommand( command )

# Apply any custom dispatch settings to the command.
Expand Down
8 changes: 8 additions & 0 deletions python/GafferTractor/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,12 @@

from .TractorDispatcher import TractorDispatcher

## Returns the `tractor.api.author` module, and must be the _only_
# method used to access that module in GafferTractor. This allows us
# to insert a mock API for testing in GafferTractorTest.
def tractorAPI() :

from tractor.api.author import author
return author

__import__( "IECore" ).loadConfig( "GAFFER_STARTUP_PATHS", subdirectory = "GafferTractor" )
1 change: 0 additions & 1 deletion python/GafferTractorTest/ModuleTest.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@

import GafferTest

@unittest.skipIf( not IECore.SearchPath( sys.path ).find( "tractor" ), "Tractor not available" )
class ModuleTest( GafferTest.TestCase ) :

def testDoesNotImportUI( self ) :
Expand Down
23 changes: 9 additions & 14 deletions python/GafferTractorTest/TractorDispatcherTest.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,10 @@
##########################################################################

import unittest
import unittest.mock
import inspect
import sys

import imath

import IECore
Expand All @@ -45,23 +47,21 @@
import GafferTest
import GafferDispatch
import GafferDispatchTest
import GafferTractor
import GafferTractorTest

@unittest.skipIf( not IECore.SearchPath( sys.path ).find( "tractor" ), "Tractor not available" )
@unittest.mock.patch( "GafferTractor.tractorAPI", new = GafferTractorTest.tractorAPI )
class TractorDispatcherTest( GafferTest.TestCase ) :

def __dispatcher( self ) :

import GafferTractor

dispatcher = GafferTractor.TractorDispatcher()
dispatcher["jobsDirectory"].setValue( self.temporaryDirectory() / "testJobDirectory" )

return dispatcher

def __job( self, nodes, dispatcher = None ) :

import GafferTractor

jobs = []
def f( dispatcher, job ) :

Expand All @@ -78,8 +78,6 @@ def f( dispatcher, job ) :

def testPreSpoolSignal( self ) :

import GafferTractor

s = Gaffer.ScriptNode()
s["n"] = GafferDispatchTest.LoggingTaskNode()

Expand Down Expand Up @@ -187,8 +185,6 @@ def testPreTasks( self ) :

def testSharedPreTasks( self ) :

import tractor.api.author as author

# n1
# / \
# i1 i2
Expand Down Expand Up @@ -221,7 +217,10 @@ def testSharedPreTasks( self ) :
self.assertEqual( job.subtasks[0].subtasks[0].subtasks[0].title, "n1 1" )
self.assertEqual( job.subtasks[0].subtasks[1].subtasks[0].title, "n1 1" )

self.assertTrue( isinstance( job.subtasks[0].subtasks[1].subtasks[0], author.Instance ) )
self.assertIsInstance(
job.subtasks[0].subtasks[1].subtasks[0],
GafferTractor.tractorAPI().Instance
)

def testTaskPlugs( self ) :

Expand All @@ -241,14 +240,10 @@ def testTaskPlugs( self ) :

def testTypeNamePrefixes( self ) :

import GafferTractor

self.assertTypeNamesArePrefixed( GafferTractor )

def testDefaultNames( self ) :

import GafferTractor

self.assertDefaultNamesAreCorrect( GafferTractor )

def testTasksWithoutCommands( self ) :
Expand Down
92 changes: 92 additions & 0 deletions python/GafferTractorTest/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,98 @@
#
##########################################################################

__mockAPI = None

# Testing replacement for `GafferTractor.tractorAPI()`. This returns
# the same `tractor.api.author` module if it is available, but otherwise
# returns a mock module sufficient for running the unit tests without
# Tractor present.
def tractorAPI() :

import types
import contextlib

# Use the real API if it is available.

with contextlib.suppress( ImportError ) :
from tractor.api.author import author
return author

# Otherwise build mock version. The goal here is not to emulate the whole
# Tractor API, but to provide the bare minimum needed to run the unit tests.

global __mockAPI
if __mockAPI is not None :
return __mockAPI

class TaskBased :

def __init__( self, **kw ) :

for key, value in kw.items() :
setattr( self, key, value )

self.subtasks = []

def addChild( self, task ) :

if task.parent is None :
self.subtasks.append( task )
task.parent = self
else :
self.subtasks.append( Instance( title = task.title ) )

class Job( TaskBased ) :

__slots__ = [ "title", "service", "envkey" ]

def asTcl( self ) :

return "# Mock serialisation"

def spool( self, block = False ) :

pass

class Task( TaskBased ) :

__slots__ = [ "title", "service", "envkey", "parent" ]

def __init__( self, **kw ) :

TaskBased.__init__( self, **kw )
self.cmds = []
self.parent = None

def addCommand( self, command ) :

self.cmds.append( command )

class Instance :

__slots__ = [ "title" ]

def __init__( self, title ) :

self.title = title

class Command :

__slots__ = [ "argv", "service", "tags" ]

def __init__( self, **kw ) :

for key, value in kw.items() :
setattr( self, key, value )

__mockAPI = types.ModuleType( "author" )
__mockAPI.Job = Job
__mockAPI.Task = Task
__mockAPI.Instance = Instance
__mockAPI.Command = Command

return __mockAPI

from .TractorDispatcherTest import TractorDispatcherTest
from .ModuleTest import ModuleTest

Expand Down
7 changes: 3 additions & 4 deletions python/GafferTractorUITest/DocumentationTest.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,14 @@

import GafferUITest
import GafferDispatch
import GafferDispatchUI
import GafferTractor
import GafferTractorUI

@unittest.skipIf( not IECore.SearchPath( sys.path ).find( "tractor" ), "Tractor not available" )
class DocumentationTest( GafferUITest.TestCase ) :

def test( self ) :

import GafferTractor
import GafferTractorUI

self.maxDiff = None
self.assertNodesAreDocumented( GafferTractor )
# Also test GafferDispatch because we add plugs to
Expand Down
5 changes: 4 additions & 1 deletion startup/gui/menus.py
Original file line number Diff line number Diff line change
Expand Up @@ -570,8 +570,11 @@ def __usdLightCreator( lightType ) :

with contextlib.suppress( ImportError ) :

import GafferTractorUI
# Raises if Tractor not available, thus avoiding registering the
# TractorDispatcher.
import tractor.api.author

import GafferTractorUI

## Metadata cleanup
###########################################################################
Expand Down
1 change: 1 addition & 0 deletions startup/gui/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ def __projectBookmark( widget, location ) :

dispatchers = [ GafferDispatch.LocalDispatcher ]
with contextlib.suppress( ImportError ) :
import tractor.api.author # Raises if Tractor not available
import GafferTractor
dispatchers.append( GafferTractor.TractorDispatcher )

Expand Down

0 comments on commit d1d6014

Please sign in to comment.