Skip to content

Writing Python Scripts for ATF Applications

Gary edited this page Mar 10, 2015 · 4 revisions

Table of Contents

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.

Setting Up Scripting

Importing Components

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

Calling On ScriptingService to Access Application Data

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.

AtfScriptVariables Component

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.

Creating Test Fixtures

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.

Python Script AddAllItems

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.

Imports

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.

Opening a Document

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:

After opening a new document, the script verifies that various counts in the new circuit are zero:
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.
The property 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.

Adding Objects to the Circuit

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.

Adding Connections to the Circuit

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.

Python Script CopyPaste

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.

Python Script DeleteLayers

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.

Test Module

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 is true.
  • False(): Test if a variable is false.
  • NotNull(): Test if a variable is not None.
  • 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.

Main Scripting Points

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 its LoadDefaultAssemblies() method, called by SetEngine() when the scripting engine is set — which should always happen).
  • Import the types you want from namespaces using ScriptingService.ImportType() and ScriptingService.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 as StandardFileCommands and the IDocumentService 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\!
In the Python scripts:
  • 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 for false, None for null, 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.

Topics in this section

Clone this wiki locally