
TestTrack Pro is
Seapine Software's flagship product. It is a defect tracking tool with slick graphical and web interfaces. It comes with a customizable process. Projects are divided into separate databases, which contain all the issue records, user records, and customizations. All features are completely customizable from within the database interface. One may choose a completely web based installation and both use and administrate the databases exclusively from one's web browser.
There is also a SOAP interface and ODBC driver available.
-- Main.DavidBaird - 15 Jun 2005
Perl Usage of SOAP Interface
The advantage of writing extensions to TestTrack in Perl is the speed of writing Perl programs and the flexibility of creating web interfaces, email interfaces and connectors to version control tools. The disadvantage is that although Perl has a
SOAP::Lite module, the module API is rather difficult to learn and limited compared to Java or C# interfaces to SOAP. This article refers to version 0.60 of SOAP::Lite. I've seen that there are substantive changes to the next version of SOAP::Lite, such that these instructions will require updating.
Preparing a TestTrack SOAP User
You will need SOAP licenses and create a special user in the TestTrack database you want to manipulate. The SOAP licenses are included in the
TestTrack Pro SDK. In my own databases, I created a user called "soapscript" and assigned it to a special "SOAP" group with permissions and access to all commands and record fields. I've seen problems with limiting the scope of commands and fields of a SOAP user, and found that using one account for all my SOAP scripting works best. Since some of my scripts change user information in the license server, I also add the SOAP account as a license server administrator.
Starting up SOAP
All information here pertains to the SOAP interface for TestTrack Pro 7.1. It should also work TestTrack Pro 6.1 and 7.0 as well. I started writing Perl scripts for TestTrack with version 5.3, but TestTrack SOAP has changed so much since then, it would not be practical to describe such an old interface.
I use
ActivePerl 5.8 for Windows, Linux and Solaris. I find it easier to write cross-platform Perl scripts when I use the same Perl for all platforms. ActivePerl has SOAP::Lite included. ActivePerl is a free distribution and may use it for commercial or non-commercial purposes, you may not redistribute it without a
written agreement with ActiveState, but you may suggest to other parties to download ActivePerl from ActiveState themselves.
The first thing you need to do is to include the SOAP::Lite module in your script
use SOAP::Lite; # +trace => 'debug';
import SOAP::Data qw/name value/;
The
+trace option is commented out, but I keep it in the code. This option is essential for debugging problems. The import of
name and
value from
SOAP::Data helps a lot to write clean code.
Next you need to create a SOAP object. You will need to know in advance the address of the TestTrack web service you want to connect to.
# Assumes that the script is run on the TestTrack server
my $wsdl = "http://localhost/ttsoapcgi.wsdl";
my $schema = 'http://www.w3.org/2001/XMLSchema';
my $soap = SOAP::Lite->service($wsdl)->xmlschema($schema);
The
$soap object is the scripts connection to the TestTrack web service. The WSDL file defined by
$wsdl will help you call the web service, but as you will see, some operations need a little more manipulation of the SOAP object. The TestTrack web service utilizes the 2001 XML schema, which is not the default for SOAP::Lite through version 0.60.
TestTrack Web Service Module
An alternative to dynamically loading the WSDL from the TestTrack server is to create and save a module file with the TestTrack web service definitions. Creating a service module is also useful for handling some problematic calls to the TestTrack SOAP methods when editing records.
First, create the module file with the stubmaker.pl script, installed in the bin directory of your Perl installation.
perl {path to perl}/bin/stubmaker.pl http://localhost/ttsoapcgi.wsdl
This will create a ttsoapcgi.pm file. Copy this file to the same directory as the script you are writing. Now that you have a module for your SOAP interface, you should add a method to automatically set the 2001 XML schema. Later I will show you how to add some XML message serialization methods to fix some problems for specific TestTrack methods. Add the following to the module file:
sub ttpro {
my $self = shift;
my $schema = 'http://www.w3.org/2001/XMLSchema';
$self->xmlschema($schema)->readable(1);
return $self;
}
Instead of calling SOAP::Lite to create your SOAP object, do the following in your script:
# The FindBin module will locate our generated ttprocgi module
use FindBin;
use lib $FindBin::RealBin;
use ttsoapcgi;
my $soap = ttsoapcgi->new->ttpro;
You can also open the TestTrack WSDL file through your favorite web browser to understand the format of methods and their returned objects.
List of Databases Exercise
Usually, I know beforehand the name of the database I need a script to connect to. However, as an excerize, one can get a list of databases without connecting to anything. This also serves as a good test that your TestTrack SOAP setup is working properly. The Seapine web site recommends the following code to get a list of databases:
my $som = $soap->getDatabaseList();
my @databases = $som->valueof('//Envelope/Body/getDatabaseListResponse/pDBList/item');
my $count = scalar(@databases);
for (my $i = 0; $i < $count; $i++)
{
print "$databases[$i]->{'name'}\n";
}
I don't know about you, but needing to understand a SOM object's structure seems a bit silly. One doesn't need to manipulate the web service through a SOM object when using a WSDL file as described above, so the same operation looks like this:
my $databases = $soap->getDatabaseList();
for my $db (@{$databases}) {
print "$db->{name}\n";
}
When utilizing a WSDL, all SOAP methods return a reference to the result,
$databases, which must be dereferenced in subsequent code,
@{$databases}.
Connecting to a Database
Very simply, you need a logon cookie, which is a scalar value used by the TestTrack web service to keep track of what user you are and which database you are accessing. The cookie is used for all SOAP methods other than
getDatabaseList() and
DatabaseLogon().
You will need to pass the username and password to the
DatabaseLogon() method, and as far as I can tell, the values will be passed as plain text to the TestTrack web service. Also, how you store the username password for your scripts I leave as an exercise. I keep my scripts reasonable secured, and anyone with administrative access to our TestTrack server can be trusted to keep these two values a secret.
The name of the database is simply the string format of the name as you see it in the TestTrack Server Administration.
my $dbname = 'Sample Database';
my $cookie = $soap->DatabaseLogon($dbname, $soapuser, $soappwd);
Error Handling
Now is a good time to explain how to handle errors from SOAP. The SOAP object contains a
call object which contains any fault messages immediately following a method call. My next example is a more involved call to
DatabaseLogon(), which allows for three attempts before failing. I wrote this because I found that in some cases, the first call to
DatabaseLogin() always failed, no matter what parameters I passed to it.
my $cookie;
for (1 .. 3) {
$cookie = $soap->DatabaseLogon($dbname, $soapuser, $soappwd);
last if not $soap->call->fault;
}
if ($soap->call->fault) {
my $faultstring = $soap->call->faultstring;
die
"$0: TestTrack login error: $faultstring\n" .
"Cannot access $dbname at $host";
}
For any other call, once logged onto a database, it is a good idea to logoff before exiting the script.
if ($soap->call->fault) {
my $faultstring = $soap->call->faultstring;
$soap->DatabaseLogoff($cookie);
die "$0: TestTrack error: $faultstring\n";
}
Debugging SOAP Messages
If you run into problems, you should examine the XML messages between your script and the TestTrack web service. To do this, change the
use statement for
SOAP::Lite.
use SOAP::Lite +trace => 'debug';
If you write script the way I do, then it is a simple matter of removing the comment on the
use line. Now, you will see all the XML messages on the screen. You can cut the any message for further examination in an XML editor, or even in your favorite web browser.
You can test problems with the web service by running the TestTrack web server binary with the XML messages:
ttsoapcgi.exe < message.xml
Furthermore, when writing email messages to Seapine support, it is best to send them sample XML messages which don't work. They can then send you XML messages which do work, and you can try to figure out how to coerce
SOAP::Lite to generate the correct message.
Returning List of Records
A very useful TestTrack SOAP method is
getRecordListForTable(). This method is the equivalent of examining list of defects in TestTrack, and selecting which columns you want to view. Any filter you can run in the web or graphical interface you can run in the SOAP interface. You should define columns in your call to
getRecordListForTable(), which greatly reduces the amount of information communicated from the web server, and thus improving your script's performance.
my $collist = name(
columnlist => [
name( item => \name( name => 'Number' )),
name( item => \name( name => 'Summary' )),
name( item => \name( name => 'Assigned To User' )),
]
)->type('ttns:CTableColumn');
my $filter = 'All Open Defects';
my $recordList = $soap->getRecordListForTable($cookie, 'Defect', $filter, $collist);
The filter you choose must already be defined in the database you are connecting to. It can either be a public filter or a filter private to the SOAP user you've defined. The names of the columns are exacly as they appear when selecting columns in the web or graphical interface.
If there are no errors, you can loop over the results. You will get a object for each record returned. Each record points to a
row array, and each row will contain a
value. The order of rows is the same as the columns requested.
for my $record (@{$recordList->{records}}) {
my $recnum = $record->{row}[0]->{value};
my $summary = $record->{row}[1]->{value};
# The assigned to column can contain more than one user, so the following will
# convert the semicolon (;) separator to a comma (,). Furthermore, it will
# change the order of the name to first name, then last name.
my $assignedto =
join ', ',
map { s/(.+), (.+)/$2 $1/; $_ }
split '\s*;\s*', $record->{row}[2]->{value};
print qq{$recnum: "$summary"\n $assignedto\n\n};
}
I wrote my own general Perl subroutine as a wrapper for
getRecordListForTable(). It takes a table name, filter name, and an array of columns input, and returns an array of rows, each row contains a hash of column names and their values.
sub record_list {
my ($table, $filter, @columninput) = @_;
# create the column list
my $collist = name( columnlist => [
map { name( item => \name( name => $_ )) } @columninput
] )->type('ttns:CTableColumn');
# call ttpro and get returned list
my $list = $soap->getRecordListForTable($cookie, $table, $filter, $collist);
die "TestTrack error: " . $soap->call->faultstring
if $soap->call->fault;
# prepare results
my @results;
# the returned columns
my @columns = map { $_ = $_->{name}; $_ } @{$list->{columnlist}};
# loop through rows and build results array
for my $row ( @{$list->{records}} ) {
my $column;
for (my $i = 0; $i < @columns; $i++) {
$column->{$columns[$i]} = ${$row->{row}}[$i]->{value};
}
push @results, $column;
}
# push results back to caller
return @results;
}
Retrieving the results of a search is now much simpler to program.
@results = record_list('Defect', 'All Open Defects', 'Number', 'Summary', 'Assigned To User');
for my $row (sort {$a->{Number} <=> $b->{Number}} @results) {
# turns off warnings on non-existant values
local $^W;
print qq{$row->{Number}: "$row->{Summary}"\n $row->{Assigned To User}\n\n};
}
Retrieving a Defect Record
There are cases where you quickly need information from a single defect, and you already know the record number or the summary of the defect. You can retrieve a defect record object with the
getDefect() method. You should open an graphical debugger or print the object through
Data::Dumper. The most important thing to remember is that
SOAP::Lite doesn't preserve all the XML types of the object, but rather converts values to Perl array references and scalars. This becomes important when you want to edit a record, which I discuss later on.
Here is an example which determines to whom a defect is assigned, or who last closed a defect:
my $defect = $soap->getDefect($cookie, $recnum, '');
die "ttpro error $message\n" if $soap->call->fault;
# look at the events in reverse order to find
# either an assignment or a closed by event
my $fullname;
for my $event (reverse @{$defect->{eventlist}}) {
if ($event->{name} eq 'Assign') {
# only copy the first name on an assignment list
$fullname = ${$event->{assigntolist}}[0];
last;
}
if ($event->{name} eq 'Close') {
$fullname = $event->{user};
last;
}
}
Finding a User in a Database
When adding or editing records, fields with user names must have the full name of the user, and not the login name. This is rather inconvenient for my needs, for example when writing an email filter, my input contains the login name of users. The problem is that there is no direct method to search for a user based on login name. Therefore, I've created the following two functions which will take either the login name or the last and first name of a user and return all three parameters from TestTrack, or return undef if the user wasn't found in the database.
To do this, I need to return an array of all users, and for each user look up the user record to compare the login name given with the one saved in the user record. This involves a lot of recursive calls within the functions, so to improve performance, I use the Memoize Perl module. My scripts assume that the user tables don't change during the short execution of the script, so I consider using Memoize to cache the results of my queries safe.
use Memoize;
# cache the queries to improve performance
sub finduser($$$);
memoize('finduser');
sub getusers();
memoize('getusers');
sub finduser($$$) {
my ( $loginname, $lastname, $firstname ) = @_;
# if lastname and firstname supplied, easy to return the loginname
if (defined $firstname and defined $lastname) {
my $user = $soap->getUser($cookie, $firstname, $lastname);
return ( exists $user->{loginname} ? ( $user->{loginname}, $lastname, $firstname ) : undef );
}
return if not defined $loginname;
# if loginname supplied, need to loop through all users in database
# and query each one for the loginname
for my $fullname ( map { $_->{row}[0]->{value} } getusers() ) {
local $^W;
( $lastname, $firstname ) = split( /,\s*/, $fullname, 2 );
return $loginname, $lastname, $firstname
if (finduser(undef, $lastname, $firstname))[0] eq $loginname;
}
return;
}
# returns a list of users' full names in the database
sub getusers() {
my $collist = name(
columnlist => [
name( item => \name( name => 'Name' ))->type('ttns:CTableColumn')
]
);
for (1 .. 3) {
my $recordList = $soap->getRecordListForTable($cookie, "User", "", $collist);
return @{$recordList->{records}} if not $soap->call->fault;
}
}
In the body of my script, I then call
finduser() like this:
($login, $lastname, $firstname) = finduser($login, undef, undef);
die "No user found for $login"
if not $lastname and not $firstname;
Adding Defects to Database
Its time to learn how to modify the TestTrack database through the web service. The first thing to learn is adding defects. A nice thing about adding defects through a script is that you can also add event records when creating a defect. Your new defect can have an estimation date and an assignment upon creation. This is useful for me when I created a customized email interface to our TestTrack databases.
When defining the defect record, you will have to specify the XML types of many values and arrays.
SOAP::Lite expects references to objects inside objects, so defining the records can get rather complicated. You also have to learn about the
eventaddorder field for events and accessing custom fields. Dates must be a string in the format 'yyyy-mm-dd' and given the
xsd:date XML type.
In the following example, I create a defect with an estimate event and assign event:
my $currentuser = 'Hooker, John Lee';
my $reported = name(
reportedbylist => [
name( item => \SOAP::Data->value(
name( comments => 'an example of adding a defect' ),
name( showorder => 0 )->type('xsd:short')
))->type('ttns:CReportedByRecord')
]
);
my $events = name(
eventlist => [
name( item => \SOAP::Data->value(
name( user => $currentuser ),
name( name => 'Estimate' ),
name( fieldlist => [
name( item => \SOAP::Data->value(
name ( name => 'Date' ),
name ( value => '2004-06-20' )->type('xsd:date')
))->type('ttns:CDateField')
])->type('ttns:CField'),
name( eventaddorder => 0 )->type('xsd:short'),
))->type('ttns:CDefectEvent'),
name( item => \SOAP::Data->value(
name( user => $currentuser ),
name( name => 'Assign' ),
name( assigntolist => [ 'Morisson, Van', 'Georgiou, Stephen Demetre' ] ),
name( eventaddorder => 1 )->type('xsd:short'),
))->type('ttns:CDefectEvent')
]
)->type('ttns:CDefectEvent');
my @dvalues = (
name( summary => 'Just an example' ),
name( enteredby => $currentuser ),
$reported,
$events
);
my $defect = name('pDefect' => \SOAP::Data->value(@dvalues))->type('ttns:CDefect');
# $soap->addDefect( $cookie, $defect ); # this causes a segmentation fault on the server
# in some versions of TestTrack
$soap->call('ttns:addDefect' => ( name( cookie => $cookie)->type('xsd:long'), $defect ));
die qq{Error adding "$summary": $faultstring\n} if $soap->call->fault;
Finding The New Defect
Often, once I create a defect in a script, I'd like to know the new defect number. Unfortunately, the
addDefect() method doesn't return this useful information, so I am forced to look it up through the
getDefect() method. I must supply the summary I used to create the defect. TestTrack allows more than one defect to define the same summary, and
getDefect() will fail if more than one defect has the same summary. So there are a few challenges to overcome in order to get the defect number of a newly created defect.
First of all the following code will retrieve the defect object, and therefore the defect number:
$defect = $soap->getDefect($cookie, '', $summary);
my $number = $soap->call->fault
? '000'
: $defect->{defectnumber};
}
If
$number is defined as
'000', then you know there are two or more defects with the same summary. I created my own subroutine to return a unique summary, by adding an integer to the name of a proposed summary. This allows me to create a new defect with a unique summary, and my previous code will most likely return a valid defect object after the defect was added to the database.
sub unique_summary {
my $summary = shift;
my $suffix = 1;
my $unique = $summary;
while (1) {
my $defect = $soap->getDefect($cookie, 0, $unique);
last if $soap->call->fault;
$suffix += 1;
$unique = "$summary $suffix";
}
return $unique;
}
Modifying Defects and Adding Events
When you want to modify an existing defect, use the
editDefect() and
saveDefect() methods. You should handle errors on
editDefect(), as TestTrack will lock a defect in the edit state. Likewise, don't keep a defect in the edit state for a long time.
The biggest problem you will encounter is that the defect object you get from
editDefect() cannot be passed to
saveDefect() without defining some serializations in
SOAP::Lite. This is because the defect object is missing all the original XML types, and when re-serialized,
SOAP::Lite makes incorrect assumptions about the correct types for fields, especially for boolean types. To overcome this problem, add the following to your
ttsoapcgi.pm Perl module created earlier:
sub SOAP::Serializer::as_int {
my $self = shift;
my($value, $name, $type, $attr) = @_;
return [$name, {'xsi:type' => 'xsd:boolean', %$attr}, 'true'] if ($name =~ /^(is|has|affects|betasite)/ && $value == 1);
return [$name, {'xsi:type' => 'xsd:boolean', %$attr}, 'false'] if ($name =~ /^(is|has|affects|betasite)/ && $value == 0);
for (qw(eventaddorder showorder testconfigtype)) {
return [$name, {'xsi:type' => 'xsd:short', %$attr}, $value] if $name eq $_;
}
for (qw(recordid cookie)) {
return [$name, {'xsi:type' => 'xsd:long', %$attr}, $value] if $name eq $_;
}
return [$name, {'xsi:type' => 'xsd:int', %$attr}, $value];
}
sub SOAP::Serializer::as_dateTime {
my $self = shift;
my($value, $name, $type, $attr) = @_;
return [$name, {'xsi:type' => 'xsd:dateTime', %$attr}, $value];
}
sub ttpro {
my $self = shift;
my $schema = 'http://www.w3.org/2001/XMLSchema';
$self->typelookup->{dateTime} = [15, sub {$_[0] =~ /^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d-\d\d:\d\d$/ }, 'as_dateTime'];
$self->xmlschema($schema)->readable(1);
return $self;
}
The new
as_int() serializer will also correct the same problems when editing user records.
Once you've added the serializers to your module, you can write some simple code to modify a defect record
my $defect = $soap->editDefect($cookie, $recnum, '');
if ( $soap->call->fault ) {
my $faultstring = $soap->call->faultstring;
warn "Unable to editing $recnum: $faultstring\n";
exit;
}
$defect->{severity} = 'High';
$soap->saveDefect($cookie, $defect);
if ( $soap->call->fault ) {
my $faultstring = $soap->call->faultstring;
warn "Error saving $param{-recnum}: $faultstring";
}
Suppose that you want to close a defect from your script, the following code will add a close event to the
$defect object from the previous example:
my $addorder = @{$defect->{eventlist}} + 1;
push @{$defect->{eventlist}},
name( item => \SOAP::Data->value(
name( name => 'Close' ),
name( user => 'Hendrix, Johnny Allen' ),
name( notes => 'Have you ever been to electric ladyland?' ),
name( date => '2005-06-20' )->type('xsd:date'),
name( hours => 0 ),
name( fieldlist => [
name( item => \SOAP::Data->value(
name ( name => 'Resolution' ),
name ( value => 'Complete' ),
))->type('ttns:CDropdownField')->attr({'xmlns:ttns' => 'urn:testtrack-interface'})
])->type('CField'),
name( eventaddorder => $addorder )->type('xsd:short')
))->type('CDefectEvent');
I spent two days debugging the
fieldlist, until I figured out how to add the correct attribute, an example of using the XML output of messages to debug problems.
Saving the Cookie
I wrote a fairly complex CGI script which allows a user who is not a member of a database to submit new defects and view the status of defects they entered. This is useful for a system conformance lab, where you may have tens of users who have no need to access a database, but you still want to allow to enter defects. A help desk TestTrack database is another situation where you want to allow anonymous entry of new records. I know about
SoloSubmit, but since I already have the SOAP licenses, and the CGI programming skills, I saw no reason I shouldn't build my own web interface.
I used one SOAP user to access the database for all of the operations. I therefore did not want to logon to the database every time a user did something in my CGI interface. I also wanted to handle a situation where two or more users would do something at the same time. I decided that I would save my TestTrack cookie and only logon if TestTrack automatically logged me out due to its internal timeout.
I created two subroutines, one to get a SOAP object, and another to get a TestTrack cookie. Here they are:
sub get_soap {
$soap = ttsoapcgi->new->ttpro;
return $soap;
}
sub get_cookie {
my $soap = shift;
$cookie = 0;
my $fh;
# the cookie is saved in a file
my $cookie_file = "$FindBin::RealBin/$FindBin::RealScript.cookie";
open $fh, '<', $cookie_file and do {
local $/;
$cookie = int(<$fh>);
close $fh;
};
# test if the cookie is still valid
$soap->getTableList($cookie);
# where the cookie isn't valid, logon to the database
if ($soap->call->fault) {
for (1 .. 3) {
$cookie = $soap->DatabaseLogon($dbName, $dbUser, $dbPass);
last if not $soap->call->fault;
}
if ($soap->call->fault) {
my $faultstring = $soap->call->faultstring;
print "TestTrack error: $faultstring<p></body></html>";
exit;
}
# the logon was successful, save the new cookie
open $fh, '>', $cookie_file and do {
print $fh $cookie;
close $fh;
};
}
return $cookie;
}
Their usage in my CGI script is rather simple
use CGI qw{:standard};
# create and initialize a CGI web page
my $cgi = new CGI;
print $cgi->header(), $cgi->start_html(-title => 'My TestTrack CGI Interface');
my $soap = get_soap();
my $cookie = get_cookie($soap);