-
Notifications
You must be signed in to change notification settings - Fork 263
Writing Python Scripts for ATF Applications
You can write Python scripts to test your application or perform other tasks that require access to the internal state of the application, such as its classes' methods and properties.
Python tests for the ATF Circuit Editor Sample illustrate these scripts and what's required to use them. This section shows some of these Python test scripts and associated code in the sample that supports scripting. For details on the programming in this sample, see Circuit Editor Programming Discussion.
First, the application must import the appropriate ATF components to support scripting. As mentioned in Scripting Components, you should generally import these components:
PythonService
ScriptConsole
AtfScriptVariables
AutomationService
Once imported, use ScriptingService
's API to get access to all the application types you need. For example, the ATF Circuit Editor Sample imports types and sets up variables in its Editor
component's IInitializable.Initialize()
method, called after all components are imported:
void IInitializable.Initialize()
{
if (m_scriptingService != null)
{
// load this assembly into script domain.
m_scriptingService.LoadAssembly(GetType().Assembly);
m_scriptingService.ImportAllTypes("CircuitEditorSample");
m_scriptingService.ImportAllTypes("Sce.Atf.Controls.Adaptable.Graphs");
m_scriptingService.SetVariable("editor", this);
m_scriptingService.SetVariable("schemaLoader", m_schemaLoader);
m_scriptingService.SetVariable("layerLister", m_layerLister);
m_contextRegistry.ActiveContextChanged += delegate
{
var editingContext = m_contextRegistry.GetActiveContext<CircuitEditingContext>();
ViewingContext viewContext = m_contextRegistry.GetActiveContext<ViewingContext>();
IHistoryContext hist = m_contextRegistry.GetActiveContext<IHistoryContext>();
m_scriptingService.SetVariable("editingContext", editingContext);
m_scriptingService.SetVariable("circuitContainer", editingContext != null ? editingContext.CircuitContainer : null);
m_scriptingService.SetVariable("view", viewContext);
m_scriptingService.SetVariable("hist", hist);
};
}
...
First, the method calls ScriptingService.LoadAssembly()
with the assembly's type to add all the namespaces in the assembly to the script domain. This allows importing all types from the "CircuitEditorSample" namespace with ScriptingService.ImportAllTypes()
in the next line. CircuitEditor uses the "CircuitEditorSample" namespace for all its code. It also imports the types from the "Sce.Atf.Controls.Adaptable.Graphs" namespace, because CircuitEditor makes heavy use of the graph classes in that namespace.
Finally, variables are set for objects it's useful to manipulate from scripts. The variable editor
is set to the Editor
object by calling the ScriptingService.SetVariable()
method. The variable schemaLoader
is set to the SchemaLoader
object, and layerLister
to the LayerLister
object.
When the active context changes, the delegate defined here sets variables for other useful objects whose value changes when the context changes. This includes editingContext
for the CircuitEditingContext
object and circuitContainer
for the CircuitEditingContext
's CircuitContainer
property.
The AtfScriptVariables
's purpose is to set variables for objects commonly found in ATF applications. In particular, it adds the variable atfDocService
for the object implementing IDocumentService
.
For the full list of variables, see AtfScriptVariables Component.
Using NUnit
in C#, you can call all test methods that have the appropriate attributes, as shown in C# Unit Tests and Python Functional Tests. This includes setting up the Main()
function to run all tests. You also need to create C# test classes showing the Python scripts you want to run.
For example, here's part of the CircuitEditorTests
class, which specifies the tests for the ATF Circuit Editor Sample. The class has the NUnit
[TestFixture]
attribute, and individual test methods have the [Test]
attribute:
[TestFixture]
public class CircuitEditorTests : TestBase
{
protected override string GetAppName()
{
return "CircuitEditor";
}
[Test]
[Category(Consts.SmokeTestCategory)]
public void CreateNewDocument()
{
string scriptPath = Path.GetFullPath(Path.Combine(@".\CommonTestScripts", "CreateNewDocument.py"));
ExecuteFullTest(scriptPath);
}
[Test]
public void AddAllItems()
{
ExecuteFullTest(ConstructScriptPath());
}
[Test]
[Category(Consts.SmokeTestCategory)]
public void EditSaveCloseAndReopen()
{
ExecuteFullTest(ConstructScriptPath());
}
[Test]
public void TestLayers()
{
ExecuteFullTest(ConstructScriptPath());
}
...
Most of methods here are like AddAllItems()
. As previously noted, FunctionalTestBase.ConstructScriptPath()
formats a Python script path name from the calling method's name. FunctionalTestBase.ExecuteFullTest()
then takes a series of steps that results in running the specified Python script.
The rest of this section describes some of the tests in CircuitEditorTests
.
The script file AddAllItems.py
adds items to a circuit canvas and verifies they are added correctly. Analyzing this script shows how you can access the CircuitEditor's objects to check its operation.
The script begins with imports:
import sys
sys.path.append("./CommonTestScripts")
import System
import Test
import CircuitEditorUtil
The operation sys.path.append
adds the CommonTestScripts
folder to the sys.path
list of search paths that the Python interpreter searches for modules. This allows importing the Test
and CircuitEditorUtil
modules in that folder, done by the last two import
statements. These modules contain utility functions that can be used in any module. CircuitEditorUtil
is useful for testing CircuitEditor; Test
is more general. For more details on what methods are available in Test
, see Test Module.
Tests often open a new document to manipulate with scripts:
doc = atfDocService.OpenNewDocument(editor)
CircuitEditorUtil.SetGlobals(schemaLoader, Schema)
The script opens a document by using the atfDocService
variable (set up by AtfScriptVariables
) holding the IDocumentService
implementer. It calls IDocumentService.OpenNewDocument()
which has this form:
IDocument OpenNewDocument(IDocumentClient client);
In the CircuitEditor sample, the IDocumentClient
implementer is the Editor
class, so OpenNewDocument()
is appropriately passed the editor
variable (set by IInitializable.Initialize()
) holding the Editor
object.
The script calls the Python function SetGlobals()
in the CircuitEditorUtil
module to set a couple of global
variables:
def SetGlobals(schemaLoader, schema):
global SchemaLoader
global Schema
SchemaLoader = schemaLoader
Schema = schema
return
SetGlobals()
sets two global variables that are used in the CircuitEditorUtil
module for the SchemaLoader
and Schema
classes. The variables used as parameters in the SetGlobals()
call are already set to the appropriate objects:
-
schemaLoader
is set in theEditor
class, as described in Calling On ScriptingService to Access Application Data. -
Schema
is also set inEditor
when it imports all the types from the "CircuitEditorSample" namespace, because theSchema
class is in "CircuitEditorSample".
Test.Equal(0, Test.GetEnumerableCount(editingContext.Items), "Verify no items at beginning")
Test.Equal(0, circuitContainer.Elements.Count, "Verify no modules at beginning")
Test.Equal(0, circuitContainer.Wires.Count, "Verify no connections at beginning")
Test.Equal(0, circuitContainer.Annotations.Count, "Verify no annotations at beginning")
The first statement checks that the item count in the current CircuitEditingContext
is zero. Recall that editingContext
is set to the CircuitEditingContext
object. This statement uses two functions in the Test
module:
-
Equal()
tests that two variables are equal. -
GetEnumerableCount()
counts the number of items in an enumerable object by enumerating them.
CircuitEditingContext.Items
gets an enumeration of all the items in this context, whose objects are then counted by GetEnumerableCount()
.
The script also checks that the object counts are zero in the circuit container, using the properties of the CircuitEditingContext
's CircuitContainer
property, held in the circuitContainer
variable. The ICircuitContainer.Elements
property, for instance, is the modifiable list of circuit elements and circuit group elements that are contained within this ICircuitContainer
object in the CircuitContainer
property.
The script now adds circuit items to the circuit and makes sure the values of various properties are what is expected:
btn = editingContext.Insert[Module](CircuitEditorUtil.CreateModuleNode("buttonType", "benjamin button"), 100, 100)
Test.Equal(1, circuitContainer.Elements.Count, "Verify module count after adding a button")
Test.Equal(circuitContainer.Elements[0].Name, "benjamin button", "Verify name")
The first statement creates a button and inserts it in the circuit. It does so by calling the CircuitEditingContext.Insert<T>()
method:
/// <summary>
/// Adds new object of given type to circuit using a transaction. Called by automated scripts.</summary>
...
public T Insert<T>(DomNode domNode, int xPos, int yPos) where T : class
The interesting thing here is that Insert<T>()
is a generic method. In Iron Python, a generic parameter is bounded by square brackets: Insert\[[T\]]()
, as seen in the first statement above. In this case, the type is Module
, because a button is of that type.
Note also that Insert<T>()
is called only by scripts, as its comment indicates. You can also create methods in your applications that are called by scripts.
The button is created by a function in CircuitEditorUtil
:
def CreateModuleNode(nodeType, name):
domNodeType = SchemaLoader.GetNodeType(Schema.NS + ":" + nodeType)
domNode = Sce.Atf.Dom.DomNode(domNodeType)
domNode.SetAttribute(Schema.moduleType.nameAttribute, nodeType)
domNode.SetAttribute(Schema.moduleType.labelAttribute, name)
This function's first line uses the global
variables SchemaLoader
and Schema
, which were set by the SetGlobals()
function, discussed in Opening a Document. It obtains the schema namespace with Schema.NS
, which is used to compose the fully qualified node type name. It calls XmlSchemaTypeLoader.GetNodeType()
with this fully qualified name to get the DomNodeType
for a given type:
public DomNodeType GetNodeType(string name)
As noted in BasicPythonService Component, BasicPythonService
's Initialize()
method imports all the types from a list of namespaces, including "Sce.Atf.Dom". Thus the constructor for DomNode
is available to create a DomNode
object, as in the second statement.
The last two script statements set the DomNode
's attributes, invoking the DomNode.SetAttribute()
method on the new DomNode
:
public void SetAttribute(AttributeInfo attributeInfo, object value)
The Schema
global
variable is used to obtain schema information for the SetAttribute()
parameters.
After the button is created, the ICircuitContainer.Elements
property is checked again to make sure that the Elements
count is one and the new element has the correct name.
The lines in AddAllItems
following these also add items and make similar checks.
After adding items, the script adds connections between items:
print "Adding connections"
btnToLight = editingContext.Connect(btn, btn.Type.Outputs[0], light, light.Type.Inputs[0], None)
Test.Equal(1, circuitContainer.Wires.Count, "Verify connection count after adding a connection")
btnToLight.Label = "button to light"
This code connects the items btn
and light
, using the IEditableGraph<Element, Wire, ICircuitPin>.Connect()
method, which is implemented in CircuitEditingContext
:
Wire IEditableGraph<Element, Wire, ICircuitPin>.Connect(
Element fromNode, ICircuitPin fromRoute, Element toNode, ICircuitPin toRoute, Wire existingEdge)
The objects btn
and light
serve as the Element
parameters, but ICircuitPin
parameters are also required to indicate which pins are connected. The Element.Type
property is a ICircuitElementType
, the circuit element type. The Outputs
and Inputs
properties in ICircuitElementType
are of type IList<ICircuitPin>
, designating lists of output and input pins. Thus, btn.Type.Outputs[0]
is the first output pin on btn
.
Note that the final Wire
parameter is None
, the Python equivalent to null
.
The ICircuitContainer.Wires
property is the modifiable list of wires that are completely contained within this circuit container. The script checks that its Count
property equals 1 after adding the connection.
The last line sets the connection's Label
property.
The subsequent lines add more connections and test the number of wires.
The script CopyPaste.py
begins similarly to AddAllItems.py
:
import sys
sys.path.append("./CommonTestScripts")
import System
import Test
import CircuitEditorUtil
CircuitEditorUtil.SetGlobals(schemaLoader, Schema)
doc = atfDocService.OpenNewDocument(editor)
#Add a button and verify
btn1 = editingContext.Insert[Module](CircuitEditorUtil.CreateModuleNode("buttonType", "button 1"), 50, 75)
Test.Equal(1, circuitContainer.Elements.Count, "verify 1 button added")
#Copy/paste the button and verify
atfEdit.Copy()
atfEdit.Paste()
Test.Equal(2, circuitContainer.Elements.Count, "verify 1st button copy/pasted")
This script imports, opens a new document, adds a button element and verifies it, just like AddAllItems
.
The last group of lines test something else: copying and pasting. The atfEdit
variable is set to a StandardEditCommands
object by the AtfScriptVariables
component, as shown in AtfScriptVariables Component.
The last added object should be selected, so StandardEditCommands.Copy()
should copy it to the clipboard. StandardEditCommands.Paste()
should paste that copy onto the circuit. The script checks that the Count
property of the ICircuitContainer.Elements
property is now 2 to reflect the pasted copy.
The DeleteLayers.py
script also adds elements to the circuit and connects them. It then attempts to create layers from elements:
print "Create layers"
Test.Equal(0, layerLister.LayeringContext.Layers.Count, "Verify no layers at the beginning")
inputs = [btn1, btn2, btn3, btn4, btn5]
layerInputs = layerLister.LayeringContext.InsertAuto(None, inputs)
layerInputs.Name = "inputs"
logic = [btn2, btn3, btn4, btn5, and1, or1, or2]
layerLogic = layerLister.LayeringContext.InsertAuto(None, logic)
layerLogic.Name = "logic"
outputs = [speaker, light]
layerOutputs = layerLister.LayeringContext.InsertAuto(None, outputs)
layerOutputs.Name = "outputs"
The variable layerLister
is set in the Editor
class's initialization to the LayerLister
object, as described in Calling On ScriptingService to Access Application Data. The script uses this variable to access the LayeringContext
and Layers
properties needed to verify there are no layers in the circuit to begin with.
After creating the inputs
array of buttons, it passes it to the LayeringContext.InsertAuto()
method (in Sce.Atf.Controls.Adaptable.Graphs
) to create a layer:
public LayerFolder InsertAuto(object parent, object objectToInsert)
{
DataObject dataObject = null;
if (objectToInsert is IEnumerable)
...
InsertAuto()
checks whether the parameter objectToInsert
is enumerable, so an enumerable object like an array is admissible. InsertAuto()
is used only by scripts.
Two more layers are created in a similar fashion. Next, the layers' visibility is tested:
print "Enable/disable layers and verify visibility"
for module in circuitContainer.Elements:
Test.True(module.Visible, "Verifying all modules are visible at beginning: " + module.Name)
layerLister.ShowLayer(layerOutputs, False)
for module in outputs:
Test.False(module.Visible, "Verify outputs are not visible after disabling outputs layer")
First, it checks the Visible
property of each Element
in the circuit to make sure it is visible. Next, LayerLister.ShowLayer()
is called to make the layer invisible, and that method has this signature:
public void ShowLayer(object layer, bool show)
The variable layerOutputs
contains the LayerFolder
object returned by LayeringContext.InsertAuto()
after the layer is created.
After hiding the layer, the script checks the Visible
property of all elements in the hidden layer (in outputs
) to verify they are invisible.
The remainder of the DeleteLayers
script uses the LayerLister
and LayeringContext
objects to verify that elements are visible after deleting layers and that the layer counts are consistent.
The Test.py
module contains test utility functions you can use in your scripts:
-
Equal()
: Test if two variables are equal. -
NotEqual()
: Test if two variables are unequal. -
True()
: Test if a variable istrue
. -
False()
: Test if a variable isfalse
. -
NotNull()
: Test if a variable is notNone
. -
GreaterThan()
: Test if one variable's string value is greater than another's. -
FuzzyCompare()
: Test if two numbers are approximately equal for a given threshold. -
GetOutputDir()
: Define a common output folder. -
GetNewFilePath()
: Construct a file path based on the input name and standard folder. -
GetEnumerableCount()
: Return the number of objects in some enumerable object. -
ConstructArray()
: For a given Python array, return the same array as a C# array.
Here are some things to keep in mind when writing scripts that interact with ATF applications.
In the application:
- Add the namespaces you need to the script domain from the appropriate assemblies using
ScriptingService.LoadAssembly()
.ScriptingService
does some of this for you (in itsLoadDefaultAssemblies()
method, called bySetEngine()
when the scripting engine is set — which should always happen). - Import the types you want from namespaces using
ScriptingService.ImportType()
andScriptingService.ImportAllTypes()
. - Create the variables needed from objects in appropriate places, as shown in Calling On ScriptingService to Access Application Data. You may need to change these variables when a context changes. The
AtfScriptVariables
component creates variable for some objects common to ATF applications, such asStandardFileCommands
and theIDocumentService
implementer. - Create functions to call if existing functions don't provide what the script needs, such as the
CircuitEditingContext.Insert<T>()
method discussed in Adding Objects to the Circuit. These should use the application's facilities though, so you are not simply testing this added function\!
- Append search paths for modules you need, as in
sys.path.append("./CommonTestScripts")
, for instance. - Import modules you need, as in
import Test
. - Use the variables created for the objects. Use them like C# objects, calling their methods and accessing their properties.
- Pass appropriate Python parameters to methods:
False
forfalse
,None
fornull
, and so on. You can pass a Python array for an enumerable object, as demonstrated in Python Script DeleteLayers. - For generic methods, put the appropriate type in square brackets, as shown in Adding Objects to the Circuit.
- Write your own utility functions and place them in a common module — or use existing functions, such as the ones described in Test Module. Check out the utility modules in ATF in
Test\FunctionalTests\CommonTestScripts
.
Development, Debugging, and Testing
-
Debugging the DOM with Visual Studio: Shows how to get
DomNode
information to help you debugging a DOM. - Visual Studio Debugger Display Attributes and Other Features: Learn about enhancing debugging in Visual Studio by using debugger display attributes and other facilities in C#.
- Using DomExplorer: Tells about a component you can use to visualize the contents of a DOM node tree.
-
Using the DomRecorder Component: Discusses the
DomRecorder
and the DOM events it records and shows an example. - General ATF Scripting Support: Explains ATF's facilities to script applications, accessing C# objects in application classes.
-
Scripting Applications with Python: Shows how to use the
BasicPythonService
andPythonService
components to script an ATF application. - ATF Test Code: Discusses the classes to facilitate writing tests for ATF-based applications as well as ATF test code.
- Writing Python Scripts for ATF Applications: Shows how to write Python scripts for ATF applications, using existing examples.
- Home
- Getting Started
- Features & Benefits
- Requirements & Dependencies
- Gallery
- Technology & Samples
- Adoption
- News
- Release Notes
- ATF Community
- Searching Documentation
- Using Documentation
- Videos
- Tutorials
- How To
- Programmer's Guide
- Reference
- Code Samples
- Documentation Files
© 2014-2015, Sony Computer Entertainment America LLC