Skip to content

Commit

Permalink
GafferTractorTest : Test using mock API if Tractor unavailable
Browse files Browse the repository at this point in the history
This allows us to run GafferTractorTest and GafferTractorUITest in CI even though we don't have Tractor installed.
  • Loading branch information
johnhaddon committed Nov 28, 2023
1 parent ae1eafa commit f7bf8f7
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 f7bf8f7

Please sign in to comment.