An unofficial SDK for building custom Alteryx tools with Go.
With the announced deprecation of the .NET SDK, a gap formed between the C/C++ and Python SDKs. C/C++ are low-level languages requiring great care and expertise to ensure proper memory management. Python is very approachable but is slower. I wanted to build tools with a middle-ground language having decent performance and simplified memory management. Go fit the bill and is my favorite language to code with.
- Prerequisites
- Installation
- Building your custom tools
- Sample tool
- Implementing the Plugin interface
- Registering your tool
- Using Provider
- Using OutputAnchor
- Using Io
- Using Environment
- Using InputConnection
- RecordInfo
- Using RecordPacket
- Testing your tools
- Feature parity with the Python SDK
- To use the Go SDK you must have Go installed on your machine. You can download the latest version of Go here.
- The Go SDK requires cgo, which means you must have a 64-bit C compiler on your system. If you do not already have one, Mingw-w64 has been tested and works with the SDK. Download from here and install, making sure Mingw-w64 is added to PATH.
- While not required, an IDE is highly recommended. I prefer the GoLand IDE from JetBrains.
Install goalteryx using Go modules: go get github.com/tlarsendataguy/goalteryx
You should specify the output DLL file and make sure -buildmode
is set to c-shared
. For reference, the following command is used to build the included example tools:
go build -o "C:\Program Files\Alteryx\bin\Plugins\goalteryx.dll" -buildmode=c-shared goalteryx/implementation_example
I build directly to the Plugins folder in the Alteryx installation folder of my dev environment. This allows me to rebuild my tools and run them directly in Alteryx without additional copying. You do not need to close and restart Alteryx when you rebuild a DLL. The next time you run a workflow with your custom tool, the new DLL will be used. It should go without saying that you should not do this in production.
The following 2 code files represent a basic tool in Alteryx that copies incoming records and pushes them through its output.
package main
import "C"
import (
"github.com/tlarsendataguy/goalteryx/sdk"
"unsafe"
)
func main() {}
//export PluginEntry
func PluginEntry(toolId C.int, xmlProperties unsafe.Pointer, engineInterface unsafe.Pointer, pluginInterface unsafe.Pointer) C.long {
plugin := &Plugin{}
return C.long(sdk.RegisterTool(plugin, int(toolId), xmlProperties, engineInterface, pluginInterface))
}
entry.go is used to register your plugin to the Alteryx engine. See plugin registration for more info.
package main
import (
"fmt"
"github.com/tlarsendataguy/goalteryx/sdk"
)
type Plugin struct {
provider sdk.Provider
output sdk.OutputAnchor
outInfo *sdk.OutgoingRecordInfo
}
func (p *Plugin) Init(provider sdk.Provider) {
provider.Io().Info(fmt.Sprintf(`Init tool %v`, provider.Environment().ToolId()))
p.provider = provider
p.output = provider.GetOutputAnchor(`Output`)
}
func (p *Plugin) OnInputConnectionOpened(connection sdk.InputConnection) {
p.provider.Io().Info(fmt.Sprintf(`got connection %v`, connection.Name()))
p.outInfo = connection.Metadata().Clone().GenerateOutgoingRecordInfo()
p.output.Open(p.outInfo)
}
func (p *Plugin) OnRecordPacket(connection sdk.InputConnection) {
packet := connection.Read()
for packet.Next() {
p.outInfo.CopyFrom(packet.Record())
p.output.Write()
}
}
func (p *Plugin) OnComplete() {
p.provider.Io().Info(`Done`)
}
plugin.go contains the implementation of your plugin. Your implementation must satisfy the Plugin interface. In this example the tool simply copies incoming records and pushes them to its output.
Plugins must implement the Plugin interface:
type Plugin interface {
Init(Provider)
OnInputConnectionOpened(InputConnection)
OnRecordPacket(InputConnection)
OnComplete()
}
The Init
function is called immediately after the tool is registered and allows you to initialize your tool. Your tool is given a Provider, which allows you to retrieve your tool's configuration, interact with the Alteryx engine, retrieve environment information, and obtain output anchors for passing records to downstream tools.
The OnInputConnectionOpened
function is called when an upstream tool is connected to your custom tool. Your tool is given an InputConnection, which allows you to retrieve the connection's name and metadata. If your custom tool is an input tool this function will not be called.
The OnRecordPacket
function is called when your custom tool recieves records from an upstream tool. Your tool is given an InputConnection, which allows you to check the incoming connection name, iterate through the incoming records, and retrieve the progress of the incoming datastream. As with OnInputConnectionOpened
, this function is not called if your custom tool is an input tool.
The OnComplete
function is called at the end of your custom tool's lifecycle. For tools which receive data from upstream tools, this happens after all incoming connections have been closed by the upstream tools. For input tools, this happens when Alteryx is ready for your tool to start processing and sending data.
Below is an example of a struct that implements the Plugin interface:
import (
"github.com/tlarsendataguy/goalteryx/sdk"
)
type Plugin struct {
provider sdk.Provider
output sdk.OutputAnchor
outInfo *sdk.OutgoingRecordInfo
}
func (p *Plugin) Init(provider sdk.Provider) {
p.provider = provider
p.output = provider.GetOutputAnchor(`Output`)
}
func (p *Plugin) OnInputConnectionOpened(connection sdk.InputConnection) {
p.outInfo = connection.Metadata().Clone().GenerateOutgoingRecordInfo()
p.output.Open(p.outInfo)
}
func (p *Plugin) OnRecordPacket(connection sdk.InputConnection) {
packet := connection.Read()
for packet.Next() {
p.outInfo.CopyFrom(packet.Record())
p.output.Write()
}
}
func (p *Plugin) OnComplete() {}
Alteryx connects to custom tools through a C API function call. All custom tools are expected to provide an entry point to the Alteryx engine that looks like the following:
long NameOfPluginEntryPoint(int nToolID, void * pXmlProperties, void *pEngineInterface, void *r_pluginInterface);
For custom Go tools, the easiest way to do this is by creating an entry file that imports the C package and exports the declared entry points that perform the necessary registration steps. Example:
package main
import "C"
import (
"github.com/tlarsendataguy/goalteryx/sdk"
"unsafe"
)
func main() {}
//export PluginEntry
func PluginEntry(toolId C.int, xmlProperties unsafe.Pointer, engineInterface unsafe.Pointer, pluginInterface unsafe.Pointer) C.long {
plugin := &Plugin{}
return C.long(sdk.RegisterTool(plugin, int(toolId), xmlProperties, engineInterface, pluginInterface))
}
We start by importing the C and unsafe packages, as well as the SDK. The next part of the file is an empty main function. DLLs are expected to have a main function, but we do not make use of it, so we can keep it empty.
The next section implements our plugin's entry point. It starts with a comment, //export PluginEntry
, which has to match the declared entry point from the tool's Config.xml file. Immediately after the comment is the function itself, with the signature the Alteryx engine is expecting.
The next line, plugin := &Plugin{}
, creates a pointer of our plugin's struct. We use that pointer in the RegisterTool
function to actually register our tool and prepare it for use.
If you have multiple tools, you can register all of them in entry.go. Example:
package main
import "C"
import (
"github.com/tlarsendataguy/goalteryx/sdk"
"unsafe"
)
func main() {}
//export FirstPlugin
func FirstPlugin(toolId C.int, xmlProperties unsafe.Pointer, engineInterface unsafe.Pointer, pluginInterface unsafe.Pointer) C.long {
plugin := &First{}
return C.long(sdk.RegisterTool(plugin, int(toolId), xmlProperties, engineInterface, pluginInterface))
}
//export SecondPlugin
func SecondPlugin(toolId C.int, xmlProperties unsafe.Pointer, engineInterface unsafe.Pointer, pluginInterface unsafe.Pointer) C.long {
plugin := &Second{}
return C.long(sdk.RegisterTool(plugin, int(toolId), xmlProperties, engineInterface, pluginInterface))
}
Registering your custom tools in this manner keeps all the registration code neatly separated from your business logic and prevents your business logic from depending on the Unsafe and C packages.
Provider
is used for obtaining information about your custom tool, sending messages to the Alteryx engine, and retrieving environmental information and variables from the Alteryx engine. It has the following interface:
type Provider interface {
ToolConfig() string
Io() Io
GetOutputAnchor(string) OutputAnchor
Environment() Environment
}
The ToolConfig
function returns the current configuration for your custom tool. It is provided as a raw XML string rather than attempting to provide a generic XML navigator object. As tool configurations are unique to each tool, it is recommended to use Go's built-in parsing capabilities to unmarshal the XML into custom structs fit for purpose.
The Io
function returns an Io, which is used primarily for sending messages to the Alteryx engine.
The GetOutputAnchor
function returns an OutgoingAnchor which you can use to send records to downstream tools.
The Environment
function returns an Environment, which you can use to obtain your custom tool's ID and retrieve environmental variables from the Alteryx engine.
OutputAnchor
is the interface you use to send data to downstream tools. It has the following interface:
type OutputAnchor interface {
Name() string
IsOpen() bool
Metadata() *OutgoingRecordInfo
Open(info *OutgoingRecordInfo)
Write()
UpdateProgress(float64)
Close()
}
The Name
function returns the name of the output anchor and should match the name provided in the tool's Config.xml file.
The IsOpen
function tells whether the Open
function has been called on the connection.
The Metadata
function returns a pointer to the OutgoingRecordInfo
that the anchor was opened with. If Open
has not been called yet, the return value is nil. See the section on RecordInfo for more information about how to use and generate OutgoingRecordInfo
structs.
The Open
function opens the output anchor and sends metadata downstream to all connected tools. The OutgoingRecordInfo
you open the connection with is also where the OutputAnchor
reads data from when Write() is called.
The Write
function writes the current values in the OutgoingRecordInfo
to downstream tools.
The UpdateProgress
function notifies downstream tools on the percentage completion of the dataset being sent. The value provided should be between 1 and 0, with 1 being 100% completed.
The Close
function writes any remaining records to downstream tools and closes the outgoing connections attached to the anchor. Calling this function is optional. All outgoing anchors and connections are closed automatically by the SDK after the OnComplete
function finishes.
Io
is the interface you use to send messages to the Alteryx engine. It has the following interface:
type Io interface {
Error(string)
Warn(string)
Info(string)
UpdateProgress(float64) bool
DecryptPassword(string) string
CreateTempFile(string) string
}
The Error
function sends an error to the Alteryx engine. This shows up in Designer as an error. When run in a unit test context, it prints an error message to stdout.
The Warn
function sends a warning to the Alteryx engine. This shows up in Designer as a warning. When run in a unit test context, it prints a warning message to stdout.
The Info
function sends a message to the Alteryx engine. This shows up in Designer as an informational message. When run in a unit test context, it prints the message to stdout.
The UpdateProgress
function notifies the Alteryx engine of the current percentage completion of the custom tool. This is the overall completion of the tool as opposed to the datastream completion percentage in the OutputAnchor.UpdateProgress()
method. The function returns a boolean. True means to continue processing and False means to stop processing. Input tools should use this function to determine whether the user clicked Cancel and prematurely stop executing.
The DecryptPassword
function decrypts a password encrypted by the front-end UI.
The CreateTempFile
function provides the path to a temporary file that can be used by the custom tool. The Alteryx engine will clean up the temporary file after the workflow finishes running. The function accepts a string argument which specifies the file extension.
Environment
is the interface you use to retrieve environment variables from the Alteryx engine. It has the following interface:
type Environment interface {
UpdateOnly() bool
UpdateMode() string
DesignerVersion() string
WorkflowDir() string
AlteryxInstallDir() string
AlteryxLocale() string
ToolId() int
UpdateToolConfig(string)
}
The UpdateOnly
function identifies whether the Alteryx engine expects the tool to send data. If the return value is true
, the tool should not send records downstream.
The UpdateMode
function returns one of a blank string, 'Quick', or 'Full'.
The DesignerVersion
function returns the version of Designer being run. If run in a unit test context, it returns the value 'TestHarness'.
The WorkflowDir
function returns the folder of the workflow the tool is being run in.
The AlteryxInstallDir
function returns the Alteryx installation folder. If run in a unit test context, it returns an empty string.
The AlteryxLocale
function returns the locale/language setting of the current user.
The ToolId
function returns the ID of the custom tool in the current workflow.
The UpdateToolConfig
function provides a way for the custom tool to update its own configuration and send it back to Designer for persistance.
InputConnection
is provided to the custom tool by the SDK and is the interface by which you interact with incoming connections and data. It has the following interface:
type InputConnection interface {
Name() string
Metadata() IncomingRecordInfo
Read() RecordPacket
Progress() float64
Status() Status
}
The Name
function returns the name of the incoming connection. This name should match the name of one of the input connections defined in the tool's Config.xml file.
The Metadata
function returns the structure of the incoming data. See RecordInfo for more information about using this interface.
The Read
function returns a RecordPacket
containing a cache of records that have been pushed to your custom tool. If you have multiple input connections, it is important to always first read the name of the input connection so you know how to process the incoming data. Input connections are not guaranteed to arrive in any specific order, nor is it guaranteed that all of an input connection's records will arrive before another input connection starts sending its data. The Read
function should only be used during the OnRecordPacket
function of the Plugin.
The Progress
function returns the percentage of records that have been passed through the InputConnection
.
The Status
function returns the current status of the incoming connection. Possible values are:
- Created: The status when the incoming connection is first registered with the tool. By the time the
OnInputConnectionOpened()
function is called on a custom tool, the input connection has already been initialized, so your custom tools should never see this status code. - Initialized: Field metadata has been received from the upstream tool. This status happens when
OnInputConnectionOpened()
is called on a custom tool. - ReceivingRecords: This status occurs as soon as the first record is received from the incoming connection.
- Closed: This status occurs when the upstream tool closes the connection. Custom tools are not directly notified when upstream connections are closed. If a custom tool needs to know when upstream connections are closed, is can check for this status in the
OnRecordPacket
andOnComplete
functions.
There are 3 different RecordInfo
structs that you may use during the lifecycle of your custom tool:
IncomingRecordInfo
EditingRecordInfo
OutgoingRecordInfo
IncomingRecordInfo
is provided during your custom tool's OnInputConnectionOpened
and OnRecordPacket
functions. It provides for a way to inspect the structure of your incoming data and generate outgoing record information that can copy data from incoming datastreams. IncomingRecordInfo
has the following interface:
func NumFields() int
func Fields() []b.FieldBase
func Clone() *EditingRecordInfo
func GetBlobField(name string) (IncomingBlobField, error)
func GetBoolField(name string) (IncomingBoolField, error)
func GetIntField(name string) (IncomingIntField, error)
func GetFloatField(name string) (IncomingFloatField, error)
func GetStringField(name string) (IncomingStringField, error)
func GetTimeField(name string) (IncomingTimeField, error)
The NumFields
function returns the number of fields in the IncomingRecordInfo
.
The Fields
function returns the list of fields. Each field provides the name, type, source, size, and scale of the field.
The Clone
function clones the IncomingRecordInfo
into an EditingRecordInfo. Using the Clone
function to build your outgoing recordinfo allows you to easily copy data from incoming records to your outgoing records.
The GetBlobField
function returns a struct that lets you extract blob values (slice of bytes) from an incoming record. This function only returns correctly if the field type of the named field is 'Blob' or 'SpatialObj'. If the field does not exist or is the incorrect type, an error is returned.
The GetBoolField
function returns a struct that lets you extract boolean values from an incoming record. This function only returns correctly if the field type of the named field is 'Bool'. If the field does not exist or is the incorrect type, an error is returned.
The GetIntField
function returns a struct that lets you extract integers from an incoming record. This function only returns correctly if the field type of the named field is 'Byte', 'Int16', 'Int32', or 'Int64'. If the field does not exist or is the incorrect type, an error is returned.
The GetFloatField
function returns a struct that lets you extract decimal numbers from an incoming record. This function only returns correctly if the field type of the named field is 'Float', 'Double', or 'FixedDecimal'. If the field does not exist or is the incorrect type, an error is returned.
The GetStringField
function returns a struct that lets you extract text values from an incoming record. This function only returns correctly if the field type of the named field is 'String', 'WString', 'V_String', or 'V_WString'. If the field does not exist or is the incorrect type, an error is returned.
The GetTimeField
function returns a struct that lets you extract temporal values from an incoming record. This function only returns correctly if the field type of the named field is 'Date' or 'DateTime'. If the field does not exist or is the incorrect type, an error is returned.
Each of the GetXxxField functions returns a field struct that provides the name, type source, size (if applicable), and scale (if FixedDecimal). The field struct also provides a GetValue
function that allows you to retrieve the field's value. The GetValue
function signatures for the various incoming fields are as follows:
IncomingBlobField: GetValue(Record) (value []byte, isNull bool)
IncomingBoolField: GetValue(Record) (value bool, isNull bool)
IncomingIntField: GetValue(Record) (value int, isNull bool)
IncomingFloatField: GetValue(Record) (value float64, isNull bool)
IncomingStringField: GetValue(Record) (value stirng, isNull bool)
IncomingTimeField: GetValue(Record) (value time.Time, isNull bool)
An example of a tool that uses GetXxxField to extract values from specific fields is below:
type Plugin struct {
field sdk.IncomingStringField
}
func (p *Plugin) Init(provider sdk.Provider) {}
func (p *Plugin) OnInputConnectionOpened(connection sdk.InputConnection) {
var err error
p.field, err = connection.Metadata().GetStringField(`MyField`)
if err != nil {
panic(`field not found or is of the wrong type`)
}
}
func (p *Plugin) OnRecordPacket(connection sdk.InputConnection) {
packet := connection.Read()
for packet.Next() {
value, isNull := p.field.GetValue(packet.Record())
}
}
func (p *Plugin) OnComplete() {}
EditingRecordInfo
is used to edit an incoming recordinfo and then generate the final outgoing recordinfo once all edits are made. It has the following interface:
func NumFields() int
func Fields() []IncomingField
func AddBoolField(name string, source string, options ...AddFieldOptionSetter) string
func AddByteField(name string, source string, options ...AddFieldOptionSetter) string
func AddInt16Field(name string, source string, options ...AddFieldOptionSetter) string
func AddInt32Field(name string, source string, options ...AddFieldOptionSetter) string
func AddInt64Field(name string, source string, options ...AddFieldOptionSetter) string
func AddFloatField(name string, source string, options ...AddFieldOptionSetter) string
func AddDoubleField(name string, source string, options ...AddFieldOptionSetter) string
func AddFixedDecimalField(name string, source string, size int, scale int, options ...AddFieldOptionSetter) string
func AddStringField(name string, source string, size int, options ...AddFieldOptionSetter) string
func AddWStringField(name string, source string, size int, options ...AddFieldOptionSetter) string
func AddV_StringField(name string, source string, size int, options ...AddFieldOptionSetter) string
func AddV_WStringField(name string, source string, size int, options ...AddFieldOptionSetter) string
func AddDateField(name string, source string, options ...AddFieldOptionSetter) string
func AddDateTimeField(name string, source string, options ...AddFieldOptionSetter) string
func AddBlobField(name string, source string, size int, options ...AddFieldOptionSetter) string
func AddSpatialObjField(name string, source string, size int, options ...AddFieldOptionSetter) string
func RemoveFields(fieldNames ...string)
func MoveField(name string, newIndex int) error
func GenerateOutgoingRecordInfo() *OutgoingRecordInfo
The NumFields
function returns the number of fields currently in the recordinfo.
The Fields
function returns a list of basic field information of all of the fields currently in the recordinfo.
The AddXxxField
functions adds a new field to the recordinfo. Each function represents a different storage type for the underlying data. All functions require a name and source, with size and scale being required on specific field types such as strings and fixed decimal fields. You may also provide a list of options when creating the field. The currently supported options are:
InsertAt(position int)
: Use this option to insert the field in the beginning or middle of the record. For example, to insert a new Int32 field at the beginning of the recordinfo, use:editor.AddInt32Field(`FieldName`, `some source`, sdk.InsertAt(0))
The RemoveFields
function removes the provided list of fields from the record, if they exist.
The MoveField
function moves a field to a different position in the record. An error is returned if newIndex
is out of bounds or if the name provided does not exist in the record.
The GenerateOutgoingRecordInfo
function returns a pointer to an OutgoingRecordInfo struct, which is used to open OutputAnchors and set values for writing to downstream tools.
OutgoingRecordInfo
is used to send metadata to downstream tools and store values that will be written to the custom tool's output anchors. You can create an OutgoingRecordInfo
from an EditingRecordInfo
or by using the NewOutgoingRecordInfo
function in the SDK. The following example creates an OutgoingRecordInfo
with a Bool field, an Int64 field, and a V_WString field:
recordInfo, fieldNames := sdk.NewOutgoingRecordInfo([]sdk.NewOutgoingField{
sdk.NewBoolField(`Field 1`, `source`),
sdk.NewInt64Field(`Field 2`, `source`),
sdk.NewV_WStringField(`Field 3`, `source`, 1000),
})
If duplicate field names are specified in the list of NewOutgoingField
, then NewOutgoingRecordInfo()
will rename the duplicate fields. The second return value from NewOutgoingRecordInfo
contains the actual field names in the OutgoingRecordInfo
.
OutgoingRecordInfo
has the following interface:
func FixedSize() int
func HasVarFields() int
func DataSize() uint32
func CopyFrom(Record)
The FixedSize
function returns the size of the fixed portion of the RecordInfo
data structure.
The HasVarFields
function identifies whether the recordinfo contains variable-length fields (V_String, V_WString, Blob, or SpatialObj).
The DataSize
functions returns the record size of the current values in the OutgoingRecordInfo
struct.
The CopyFrom
function copies values from the incoming record into its current values. This function only copies those fields which originated from an IncomingRecordInfo
via the Clone
method.
The following code shows an end-to-end example of how to use the various recordinfo structs by implementing a custom tool that adds a record ID to the beginning of the record.
package awesomeProject
import (
"github.com/tlarsen7572/goalteryx/sdk"
)
type Plugin struct {
outputAnchor sdk.OutputAnchor
outputInfo *sdk.OutgoingRecordInfo
recordIdFieldName string
recordId int
}
func (p *Plugin) Init(provider sdk.Provider) {
p.outputAnchor = provider.GetOutputAnchor(`Output`)
p.recordId = 0
}
func (p *Plugin) OnInputConnectionOpened(connection sdk.InputConnection) {
// convert the incoming recordinfo into an editor
editor := connection.Metadata().Clone()
// add the record ID field
p.recordIdFieldName = editor.AddInt32Field(`RecordId`, `my custom tool`, sdk.InsertAt(0))
// generate the outgoing recordinfo
p.outputInfo = editor.GenerateOutgoingRecordInfo()
// open the output anchor with the metadata from the outgoing recordinfo
p.outputAnchor.Open(p.outputInfo)
}
func (p *Plugin) OnRecordPacket(connection sdk.InputConnection) {
packet := connection.Read()
for packet.Next() {
// copy data from the incoming record to the current values of the outgoing recordinfo
p.outputInfo.CopyFrom(packet.Record())
// set the record ID field
p.outputInfo.IntFields[p.recordIdFieldName].SetInt(p.recordId)
// write the current outgoing recordinfo values to downstream tools
p.outputAnchor.Write()
p.recordId++
}
}
func (p *Plugin) OnComplete() {}
RecordPacket
is an abstraction used to iterate through the packet of records sent to your custom tool by upstream tools. Records are recieved (and sent) in 4mb chunks. This is done to minimize the number of calls between the Alteryx engine and the Go runtime, each of which has bookkeeping overhead.
RecordPacket
has the following interface:
func Next() bool
func Record() Record
The Next
function tries to retrieve the next record in the packet. If there are no more records, it returns false; otherwise, it returns true.
The Record
function returns the record retrieved during the call to Next
.
The easiest way to interact with RecordPacket
is to iterate through it using a for loop:
func iteratePacket(packet RecordPacket) {
for packet.Next() {
record := packet.Record()
// do something with the record
}
}
GoAlteryx includes testing facilities to assist your development of custom tools. They are designed to mimic the lifecycle events your tool will experience during the run of a workflow. As a result, you can develop and test your tools without running them in Alteryx and still be confident that they will work. This also frees the developer to choose non-Windows development environments such as macOS.
A basic example of unit testing input and passthrough tools is below:
package awesomeProject_test
import (
"awesomeProject"
"github.com/tlarsen7572/goalteryx/sdk"
"testing"
)
func TestInputTool(t *testing.T) {
plugin := &awesomeProject.InputPlugin{}
runner := sdk.RegisterToolTest(plugin, 1, `<Configuration></Configuration>`)
collector := runner.CaptureOutgoingAnchor(`Output`)
runner.SimulateLifecycle()
t.Logf(`%v`, collector.Data)
}
func TestPassthroughTool(t *testing.T) {
plugin := &awesomeProject.PassthroughPlugin{}
runner := sdk.RegisterToolTest(plugin, 1, `<Configuration></Configuration>`)
collector := runner.CaptureOutgoingAnchor(`Output`)
runner.ConnectInput(`Input`, `testfile.txt`)
runner.SimulateLifecycle()
t.Logf(`%v`, collector.Data)
}
In both cases, the unit test begins by creating a pointer to your plugin and then registering it with the test harness by calling sdk.RegisterToolTest()
. You provide the pointer, tool ID, and configuration XML as a string in the registration call. The registration function returns a test runner. Using the test runner, you can capture outgoing records and connect input testing files to your custom tools. Calling SimulateLifecycle()
on the runner will then execute the lifecycle events and test your tool. You can inspect and verify your custom tools' outputs by inspecting the Data
member on the captured outgoing anchor.
A detailed review of the test harness features are below. We start with the signature of the RegisterToolTest
function:
func RegisterToolTest(plugin Plugin, toolId int, xmlProperties string, optionSetters ...OptionSetter) *FileTestRunner
plugin
is a struct that fulfills the Plugin interface specified by the SDK.
toolId
is an arbitrary integer that represents the tool's ID when it is placed on a workflow's canvas.
xmlProperties
is a string containing the XML configuration you want your tool to receive for the test.
optionSetters
is a list of options to pass to the test harness. The following options are currently available:
func UpdateOnly(bool)
: Sets the engine's UpdateOnly environment variablefunc UpdateMode(string)
: Sets the engine's UpdateMode environment variablefunc WorkflowDir(string)
: Sets a custom workflow directory for the testfunc AlteryxLocal(string)
: Sets the locale for the test
Any, all, or no options may be specified. An example of registering a tool with the test harness that specifies the UpdateOnly and AlteryxLocale options is below:
runner := sdk.RegisterToolTest(plugin, 1, `<Configuration></Configuration>`, sdk.UpdateOnly(true), sdk.AlteryxLocale(`en-us`))
The RegisterToolTest
function returns a pointer to a FileTestRunner
. The interface for FileTestRunner
is:
func CaptureOutgoingAnchor(name string) *RecordCollector
func ConnectInput(name string, dataFile string)
func SimulateLifecycle()
The CaptureOutgoingAnchor
function adds an outgoing connection to the specified anchor of your tool. It returns a pointer to a RecordCollector
, which you can use to inspect the data output from your tool. Retrieving RecordCollector.Data
will return a map[string][]interface{}
containing the output data. The map key is the output field name and the map value is a list of interface{}
containing the values that were output for that field.
The ConnectInput
function connects input data to the specified anchor of your tool. You specify the path to a data file in the second argument. Data files can be best thought of as pipe-delimited files with a few special rules. The rules to follow are:
- The first row must contain the field names
- The second row must contain the field types
- Bool, integer, decimal, date, and binary fields should not be quoted
- Strings should be double-quoted if leading/trailing whitespace is desired or the field value contains a pipe
- If a string field needs a double quote in the value, escape it with a backslash (\")
- If a string field needs a backslash in the value, escape it with a backslash (\\)
- You may use
\r
and\n
to specify carriage return and newline characters - Leading or trailing spaces outside of double quotes is ignored
- Empty fields are interpreted as nulls
- String fields with a value of 2 double quotes ("") are interpreted as empty strings rather than null
- Dates should be entered with a
YYYY-mm-dd
format - DateTimes should be entered with a
YYY-mm-dd HH:MM:SS
format - Size and scale, for fields that require them, are specified after the field type and separated by semi-colons (;)
An example data file is below that illustrates how to set up each different type of field and how the data should be formatted.
Field1|Field2|Field3|Field4|Field5|Field6|Field7|Field8 |Field9 |Field10 |Field11 |Field12 |Field13 |Field14 |Field15|Field16
Bool |Byte |Int16 |Int32 |Int64 |Float |Double|FixedDecimal;19;2|String;100|WString;100|V_String;10000|V_WString;100000|Date |DateTime |Blob;10|SpatialObj;100
true |2 |100 |1000 |10000 |12.34 |1.23 | 234.56 |"ABC" |"Hello " |" World" |"abcdefg" |2020-01-01|2020-01-02 03:04:05| |
false |-2 |-100 |-1000 |-10000|-12.34|-1.23 | -234.56 |"DE|\"FG" |HIJK | LMNOP |"QRSTU\r\nVWXYZ"|2020-02-03|2020-01-02 13:14:15| |
| | | | | | | | | | | | | | |
true |42 |-110 |392 |2340 |12 |41.22 | 98.2 |"" |"HIJK" | LMN |"qrstuvwxyz" |2020-02-13|2020-11-02 13:14:15| |
The graph below identifies elements of the Python SDK API that are implemented, or not implemented, in goalteryx.
🟢 = Implemented, 🟡 = Not implemented, but planned, ⚪ = Not planned for implementation
-
🟢 Plugin
- 🟢 Init
- 🟢 OnInputConnectionOpened
- 🟢 OnRecordPacket
- 🟢 OnComplete
-
🟢 Provider
- 🟢 ToolConfig
- ⚪ Logger
- 🟢 IO
- 🟢 Environment
- ⚪ GetInputAnchor
- 🟢 GetOutputAnchor
-
🟢 IO
- 🟢 Error
- 🟢 Warn
- 🟢 Info
- 🟢 UpdateProgress
- 🟢 CreateTempFile
- 🟢 DecryptPassword
-
🟢 Environment
- 🟢 UpdateOnly
- 🟢 UpdateMode
- 🟢 DesignerVersion
- 🟢 WorkflowDir
- 🟢 AlteryxInstallDir
- 🟢 Locale
- 🟢 ToolId
- 🟢 UpdateToolConfig
-
🟢 OutputAnchor
- 🟢 Name
- ⚪ AllowMultiple
- ⚪ Optional
- 🟢 NumConnections
- 🟢 IsOpen
- 🟢 Metadata
- 🟢 Open
- 🟢 Write
- ⚪ Flush
- 🟢 Close
- 🟢 UpdateProgress
-
⚪ InputAnchor
- ⚪ Name
- ⚪ AllowMultiple
- ⚪ Optional
- ⚪ Connections
-
🟢 InputConnection
- 🟢 Name
- 🟢 Metadata
- ⚪ Anchor
- 🟢 Read
- ⚪ MaxPacketSize
- 🟢 Progress
- 🟢 Status
-
RecordPacket
- RecordPacket is intentionally different than the Python implementation. Python translates record packets to and from data frames. This makes sense for Python tools, but not for Go. The Go implementation of RecordPacket mimics the behavior of the Go SQL package. Records in a record packet are accessed through an iterator and field-specific extractors.