You've already forked UnrealEngineUWP
mirror of
https://github.com/izzy2lost/UnrealEngineUWP.git
synced 2026-03-26 18:15:20 -07:00
Please note that file comments had no purpose in nearly all cases and just added visual clutter. The two files that had meaningful file comments had their comments moved into the corresponding classes. There are still hundreds of file comments left in other files that will be removed over time. Also cleaned up some random stuff along the way: - relative paths to public headers within the same module are no longer necessary (automatically discovered by UBT now) - header guards are deprecated, use #pragma once instead (all compilers support it now) - space between multiple template brackets is no longer required (all compilers support >> now) - NULL to nullptr, OVERRIDE to override - spelling errors, whitespace, line breaks [CL 2104067 by Max Preussner in Main branch]
986 lines
30 KiB
C++
986 lines
30 KiB
C++
// Copyright 1998-2014 Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "TaskBrowserPrivatePCH.h"
|
|
#include "TestTrackTaskDatabaseProvider.h"
|
|
|
|
|
|
#if WITH_TESTTRACK
|
|
|
|
// TestTrack includes
|
|
#include "AllowWindowsPlatformTypes.h"
|
|
#include "ThirdParty/TestTrack/Source/TestTrackSoapCGIProxy.h"
|
|
#include "HideWindowsPlatformTypes.h"
|
|
|
|
// TestTrack library
|
|
#if UE_BUILD_DEBUG
|
|
#if PLATFORM_64BITS
|
|
#pragma comment( lib, "ThirdParty/TestTrack/Lib/Debug/TestTrack_64.lib" )
|
|
#else
|
|
#pragma comment( lib, "ThirdParty/TestTrack/Lib/Debug/TestTrack.lib" )
|
|
#endif
|
|
#else
|
|
#if PLATFORM_64BITS
|
|
#pragma comment( lib, "ThirdParty/TestTrack/Lib/Release/TestTrack_64.lib" )
|
|
#else
|
|
#pragma comment( lib, "ThirdParty/TestTrack/Lib/Release/TestTrack.lib" )
|
|
#endif
|
|
#endif
|
|
|
|
|
|
/**
|
|
* Static: Creates a new instance of a TestTrack task database object
|
|
*
|
|
* @return The new database if successful, otherwise NULL
|
|
*/
|
|
FTestTrackProvider* FTestTrackProvider::CreateTestTrackProvider()
|
|
{
|
|
FTestTrackProvider* NewDatabase = new FTestTrackProvider();
|
|
check( NewDatabase != NULL );
|
|
|
|
// Initialize the database
|
|
if( !NewDatabase->Init() )
|
|
{
|
|
delete NewDatabase;
|
|
return NULL;
|
|
}
|
|
|
|
return NewDatabase;
|
|
}
|
|
|
|
|
|
/**
|
|
* FTestTrackProvider constructor
|
|
*/
|
|
FTestTrackProvider::FTestTrackProvider()
|
|
: TestTrackSoap( NULL ),
|
|
LastErrorMessage( TEXT( "No error" ) ),
|
|
bLastErrorWasCausedByDisconnection( false ),
|
|
ANSIServerEndpointURLString( NULL ),
|
|
IDCookie( 0 ),
|
|
UserRealName()
|
|
{ }
|
|
|
|
|
|
/**
|
|
* FTestTrackProvider destructor
|
|
*/
|
|
FTestTrackProvider::~FTestTrackProvider()
|
|
{
|
|
TDLOG( TEXT( "TestTrack: Destroying TestTrack provider" ) );
|
|
|
|
// Destroy SOAP interface to TestTrack Pro
|
|
if( TestTrackSoap != NULL )
|
|
{
|
|
// NOTE: The TestTrack soap destructor will call soap_destroy(), soap_end() and soap_free()
|
|
// will clean up allocated memory
|
|
delete TestTrackSoap;
|
|
TestTrackSoap = NULL;
|
|
}
|
|
|
|
// Clean up endpoint URL string
|
|
if( ANSIServerEndpointURLString != NULL )
|
|
{
|
|
delete[] ANSIServerEndpointURLString;
|
|
ANSIServerEndpointURLString = NULL;
|
|
}
|
|
|
|
TDLOG( TEXT( "TestTrack: TestTrack provider finished destroying" ) );
|
|
}
|
|
|
|
|
|
/**
|
|
* Initializes this task database
|
|
*
|
|
* @return true if successful
|
|
*/
|
|
bool FTestTrackProvider::Init()
|
|
{
|
|
TDLOG( TEXT( "TestTrack: Initializing TestTrack provider" ) );
|
|
|
|
|
|
// Allocate SOAP interface to TestTrack Pro
|
|
TestTrackSoap = new ttsoapcgi();
|
|
check( TestTrackSoap != NULL );
|
|
|
|
|
|
// @todo: Are there any other SOAP options we want to be configuring here?
|
|
|
|
|
|
// Disable multi-referenced strings (href="#_12"). This makes the XML structures larger but
|
|
// TestTrack doesn't support resolving graph references. This effectively changes the graph
|
|
// to a tree with duplicated strings where needed.
|
|
soap_clr_omode( TestTrackSoap->soap, SOAP_XML_GRAPH );
|
|
soap_set_omode( TestTrackSoap->soap, SOAP_XML_TREE );
|
|
|
|
|
|
// Set timeouts for SOAP network I/O
|
|
TestTrackSoap->soap->recv_timeout = 10000; /* when > 0, gives socket recv timeout in seconds, < 0 in usec */
|
|
TestTrackSoap->soap->send_timeout = 4000; /* when > 0, gives socket send timeout in seconds, < 0 in usec */
|
|
TestTrackSoap->soap->connect_timeout = 4000; /* when > 0, gives socket connect() timeout in seconds, < 0 in usec */
|
|
TestTrackSoap->soap->accept_timeout = 4000; /* when > 0, gives socket accept() timeout in seconds, < 0 in usec */
|
|
|
|
|
|
TDLOG( TEXT( "TestTrack: TestTrack provider finished initializing" ) );
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
/**
|
|
* Sets the TestTrack server endpoint address using the specified URL string
|
|
*
|
|
* @param InServerURL The server URL address
|
|
*/
|
|
void FTestTrackProvider::SetupServerEndpoint( const FString& InServerURL )
|
|
{
|
|
TDLOG( TEXT( "TestTrack: %s" ), *FString( __FUNCTION__ ) );
|
|
|
|
// Cleanup old endpoint string
|
|
if( ANSIServerEndpointURLString != NULL )
|
|
{
|
|
delete[] ANSIServerEndpointURLString;
|
|
ANSIServerEndpointURLString = NULL;
|
|
}
|
|
|
|
// Create a URL string for the server's CGI binary
|
|
FString ServerEndPointURL = InServerURL + TEXT( "/cgi-bin/ttsoapcgi.exe" );
|
|
ANSIServerEndpointURLString = new ANSICHAR[ ServerEndPointURL.Len() + 1 ];
|
|
FCStringAnsi::Strcpy( ANSIServerEndpointURLString, ServerEndPointURL.Len() + 1, TCHAR_TO_ANSI( *ServerEndPointURL ) );
|
|
|
|
// Point the SOAP library toward the server endpoint
|
|
TestTrackSoap->endpoint = ANSIServerEndpointURLString;
|
|
}
|
|
|
|
|
|
/**
|
|
* Checks the result code and returns whether the operation succeeded. For failure cases, keeps track
|
|
* of the actual error message so it can be retrieved later.
|
|
*
|
|
* @param ResultCode The result code to test
|
|
*
|
|
* @return True if the result code reported success
|
|
*/
|
|
bool FTestTrackProvider::VerifyTTPSucceeded( const int32 ResultCode )
|
|
{
|
|
if( ResultCode != SOAP_OK )
|
|
{
|
|
TDLOG( TEXT( "TestTrack: Error (ResultCode: %i)" ), ResultCode );
|
|
|
|
if( TestTrackSoap->soap->fault != NULL )
|
|
{
|
|
TDLOG( TEXT( "TestTrack: Error details (faultcode: '%s', faultstring: '%s', detail: '%s')" ),
|
|
*FString( TestTrackSoap->soap->fault->faultcode ),
|
|
*FString( TestTrackSoap->soap->fault->faultstring ),
|
|
*FString( TestTrackSoap->soap->fault->detail->__any ) );
|
|
|
|
// Store the error message string so it can be reported back to the user later
|
|
LastErrorMessage = FString::Printf(
|
|
TEXT( "%s (%s)" ),
|
|
*FString( TestTrackSoap->soap->fault->faultstring ),
|
|
*FString( TestTrackSoap->soap->fault->faultcode ) );
|
|
}
|
|
else
|
|
{
|
|
// Store the error message string so it can be reported back to the user later
|
|
LastErrorMessage = FString::Printf(
|
|
TEXT( "TestTrack SOAP API reports a result code of %i" ),
|
|
ResultCode );
|
|
}
|
|
|
|
|
|
// Check to see if this was a disconnection error
|
|
// @todo: Are there other SOAP or TestTrack error codes that indicate disconnection? Handle them here!
|
|
bLastErrorWasCausedByDisconnection = false;
|
|
{
|
|
// Handle the following error:
|
|
// ResultCode: 2, faultcode: 'SOAP-ENV:Server', faultstring: 'Session Dropped.', detail: '17'
|
|
if( ResultCode == 2 &&
|
|
TestTrackSoap->soap->fault != NULL &&
|
|
FCString::Atoi( *FString( TestTrackSoap->soap->fault->detail->__any ) ) == 17 )
|
|
{
|
|
bLastErrorWasCausedByDisconnection = true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
LastErrorMessage = TEXT( "No error" );
|
|
bLastErrorWasCausedByDisconnection = false;
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
/**
|
|
* Queries the server for a list of databases that the user has access to. This can be called before
|
|
* the user is logged into the server
|
|
*
|
|
* @param InServerURL The server URL address
|
|
* @param InUserName User name string
|
|
* @param InPassword Password string
|
|
* @param OutDatabaseNames List of available database names
|
|
*
|
|
* @return True if successful
|
|
*/
|
|
bool FTestTrackProvider::QueryAvailableDatabases( const FString& InServerURL, const FString& InUserName, const FString& InPassword, TArray< FString >& OutDatabaseNames )
|
|
{
|
|
TDLOG( TEXT( "TestTrack: %s" ), *FString( __FUNCTION__ ) );
|
|
|
|
OutDatabaseNames.Empty();
|
|
|
|
|
|
// Set the server end point
|
|
SetupServerEndpoint( InServerURL );
|
|
|
|
|
|
// Query list of all TTP databases on the server
|
|
TT1__getDatabaseListResponse TTPDatabaseList;
|
|
int32 ResultCode = TestTrackSoap->TT1__getDatabaseList( TTPDatabaseList ); // Out
|
|
if( !VerifyTTPSucceeded( ResultCode ) )
|
|
{
|
|
// Failed to get database list
|
|
return false;
|
|
}
|
|
|
|
|
|
if( TTPDatabaseList.pDBList != NULL )
|
|
{
|
|
for( int32 CurDBIndex = 0; CurDBIndex < TTPDatabaseList.pDBList->__size; ++CurDBIndex )
|
|
{
|
|
#if LOG_TASK_DATABASE
|
|
TDLOG( TEXT( "Found database: %s" ), *FString( TTPDatabaseList.pDBList->__ptritem[ CurDBIndex ]->name ) );
|
|
#endif
|
|
|
|
OutDatabaseNames.Add( FString( TTPDatabaseList.pDBList->__ptritem[ CurDBIndex ]->name ) );
|
|
}
|
|
}
|
|
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
/**
|
|
* Attempts to connect and login to the specified database
|
|
*
|
|
* @param InServerURL The server URL address
|
|
* @param InUserName User name string
|
|
* @param InPassword Password string
|
|
* @param InDatabaseName Name of the database to connect to
|
|
* @param OutUserRealName [Out] The real name of our user
|
|
* @param OutResolutionValues [Out] List of valid fix resolution values
|
|
* @param OutOpenTaskStatusPrefix [Out] Name of task status for 'Open' tasks
|
|
*
|
|
* @return True if successful
|
|
*/
|
|
bool FTestTrackProvider::ConnectToDatabase( const FString& InServerURL, const FString& InUserName, const FString& InPassword, const FString& InDatabaseName, FString& OutUserRealName, TArray< FString >& OutResolutionValues, FString& OutOpenTaskStatusPrefix )
|
|
{
|
|
TDLOG( TEXT( "TestTrack: %s" ), *FString( __FUNCTION__ ) );
|
|
|
|
OutUserRealName = TEXT( "" );
|
|
OutResolutionValues.Empty();
|
|
|
|
// TestTrack always uses "Open" for open tasks (also, "Open (Verify Failed)")
|
|
OutOpenTaskStatusPrefix = TEXT( "Open" );
|
|
|
|
// Set the server end point
|
|
SetupServerEndpoint( InServerURL );
|
|
|
|
LONG64 MyCookie = 0;
|
|
|
|
// @todo: DatabaseLogon is the deprecated way to login to a project, but ProjectLogon doesn't seem to work
|
|
int32 ResultCode = TestTrackSoap->TT1__DatabaseLogon(
|
|
TCHAR_TO_ANSI( *InDatabaseName ),
|
|
TCHAR_TO_ANSI( *InUserName ),
|
|
TCHAR_TO_ANSI( *InPassword ),
|
|
MyCookie ); // Out
|
|
|
|
|
|
if( !VerifyTTPSucceeded( ResultCode ) )
|
|
{
|
|
// Login failed
|
|
return false;
|
|
}
|
|
|
|
// Store the new ID cookie
|
|
IDCookie = MyCookie;
|
|
|
|
TDLOG( TEXT( "TestTrack: Logged in as '%s' (IDCookie: %lld)" ), *InUserName, IDCookie );
|
|
|
|
|
|
|
|
|
|
// Check event definitions for custom dropdown field names
|
|
TDLOG( TEXT( "TestTrack: Querying event definitions to gather custom field data" ) );
|
|
{
|
|
TT1__getEventDefinitionListResponse TTPEventDefs;
|
|
ResultCode = TestTrackSoap->TT1__getEventDefinitionList(
|
|
MyCookie,
|
|
"Defect", // Table name
|
|
TTPEventDefs ); // Out
|
|
|
|
if( TTPEventDefs.EventDefinitionList != NULL )
|
|
{
|
|
for( int CurEventIndex = 0; CurEventIndex < TTPEventDefs.EventDefinitionList->__size; ++CurEventIndex )
|
|
{
|
|
TT1__CEventDefinition* CurTTPEventDef = TTPEventDefs.EventDefinitionList->__ptritem[ CurEventIndex ];
|
|
|
|
// We're looking for custom fields for "Fix" event types
|
|
if( FString( CurTTPEventDef->name ) == TEXT( "Fix" ) ) // This is a standard TTP even type
|
|
{
|
|
// Any custom fields for this event type?
|
|
if( CurTTPEventDef->customFields != NULL )
|
|
{
|
|
for( int CurCustomFieldIndex = 0; CurCustomFieldIndex < CurTTPEventDef->customFields->__size; ++CurCustomFieldIndex )
|
|
{
|
|
TT1__CField* CurTTPCustomField = CurTTPEventDef->customFields->__ptritem[ CurCustomFieldIndex ];
|
|
|
|
// We're looking for dropdown values for the 'Resolution' custom event
|
|
const FString ResolutionCustomFieldName = TEXT( "Resolution" ); // This is our custom event field
|
|
if( CurTTPCustomField->soap_type() == SOAP_TYPE_TT1__CDropdownField &&
|
|
FString( CurTTPCustomField->name ) == ResolutionCustomFieldName )
|
|
{
|
|
TDLOG( TEXT( "TestTrack: Found Resolution custom field for Fix event" ) );
|
|
|
|
TT1__CDropdownField* TTPDropdownField =
|
|
static_cast< TT1__CDropdownField* >( CurTTPCustomField );
|
|
|
|
// Any dropdown values?
|
|
if( TTPDropdownField->dropdownValues != NULL )
|
|
{
|
|
for( int CurDropdownValue = 0; CurDropdownValue < TTPDropdownField->dropdownValues->__size; ++CurDropdownValue )
|
|
{
|
|
// Found a dropdown value!
|
|
FString ResolutionOption = TTPDropdownField->dropdownValues->__ptritem[ CurDropdownValue ]->value;
|
|
|
|
TDLOG( TEXT( "TestTrack: Found Resolution dropdown value: %s" ), *ResolutionOption );
|
|
|
|
OutResolutionValues.AddUnique( ResolutionOption );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// Figure out what our real name is (so we can use it to filter tasks, etc.)
|
|
TDLOG( TEXT( "TestTrack: Querying user list to find user's real name" ) );
|
|
{
|
|
TT1__getGlobalUserListResponse TTPUserList;
|
|
ResultCode = TestTrackSoap->TT1__getGlobalUserList(
|
|
IDCookie,
|
|
TTPUserList ); // Out
|
|
|
|
//
|
|
// NOTE: If this returns the following error:
|
|
// "You do not have sufficient License Server security clearance to perform this function."
|
|
//
|
|
// You'll need to change the permission settings in the TestTrack License Server Admin Utility
|
|
// such that users can have permission to View Global Users.
|
|
//
|
|
|
|
// @todo: It would be great if Seapine could give us the user's real name at connection time
|
|
// instead of having to query
|
|
|
|
if( VerifyTTPSucceeded( ResultCode ) )
|
|
{
|
|
if( TTPUserList.GlobalUserList != NULL )
|
|
{
|
|
// Find the current user in the list
|
|
for( int32 CurUserIndex = 0; CurUserIndex < TTPUserList.GlobalUserList->__size; ++CurUserIndex )
|
|
{
|
|
const TT1__CGlobalUser& CurUser = *TTPUserList.GlobalUserList->__ptritem[ CurUserIndex ];
|
|
|
|
if( CurUser.loginname != NULL && FString( CurUser.loginname ) == InUserName )
|
|
{
|
|
// Make sure we have a valid real name
|
|
if( CurUser.name != NULL && FString( CurUser.name ).Len() > 0 )
|
|
{
|
|
// Great, we found our real name!
|
|
OutUserRealName = CurUser.name;
|
|
|
|
TDLOG( TEXT( "TestTrack: Found my real name: %s" ), *FString( CurUser.name ) );
|
|
|
|
|
|
#if 0
|
|
// TestTrack has a per-user option for how the user's real name should be displayed,
|
|
// either 'First Middle Last' or 'Last, First Middle'. We'll try to convert it here.
|
|
int32 CommaPos = OutUserRealName.Find( TEXT( "," ) );
|
|
if( CommaPos != INDEX_NONE )
|
|
{
|
|
// OK, the user's real name was reported in the 'Last, First Middle' format.
|
|
FString ReversedRealName = OutUserRealName;
|
|
|
|
// Grab text after the comma ('First Middle')
|
|
OutUserRealName = ReversedRealName.Right( ReversedRealName.Len() - ( CommaPos + 2 ) );
|
|
|
|
// Append a space
|
|
OutUserRealName += TEXT( " " );
|
|
|
|
// Append text before the comma ('Last')
|
|
OutUserRealName += ReversedRealName.Left( CommaPos );
|
|
|
|
TDLOG( TEXT( "TestTrack: Converted real name to 'First Middle Last' format: %s" ), *OutUserRealName );
|
|
}
|
|
#endif
|
|
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if( OutUserRealName.Len() == 0 )
|
|
{
|
|
// User name wasn't found in the list -- this should never happen!
|
|
TDLOG( TEXT( "TestTrack: Couldn't find real name for user!" ) );
|
|
}
|
|
}
|
|
|
|
if( OutUserRealName.Len() == 0 )
|
|
{
|
|
// Couldn't query user list. This isn't a fatal error, it just means we won't have access
|
|
// to the user's real name, which may restrict our ability to manually filter tasks.
|
|
|
|
// Server wasn't able to retrieve the user's real name (probably due to permissions issues),
|
|
// so we'll try to use the user's login name as the real name for filtering
|
|
OutUserRealName = InUserName;
|
|
|
|
// @todo: We may need to find a better way to get the user's real name in this case
|
|
|
|
// @todo: This will never give us the correct name when the user's TTP options are set to "Last, First Middle" mode
|
|
|
|
// Convert . characters in login name to spaces to get the real name
|
|
OutUserRealName.ReplaceInline( TEXT( "." ), TEXT( " " ) );
|
|
|
|
// Convert to mixed case
|
|
for( int32 CurCharIndex = 0; CurCharIndex < OutUserRealName.Len(); ++CurCharIndex )
|
|
{
|
|
if( CurCharIndex == 0 || OutUserRealName[ CurCharIndex - 1 ] == TCHAR( ' ' ) )
|
|
{
|
|
OutUserRealName[ CurCharIndex ] = FChar::ToUpper( OutUserRealName[ CurCharIndex ] );
|
|
}
|
|
}
|
|
|
|
|
|
TDLOG( TEXT( "TestTrack: Tried to guess at user's real name for %s, as: %s" ), *InUserName, *OutUserRealName );
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Store the user real name so we can use it later for marking tasks as fixed
|
|
UserRealName = OutUserRealName;
|
|
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
/**
|
|
* Logs the user off and disconnects from the database
|
|
*
|
|
* @return True if successful
|
|
*/
|
|
bool FTestTrackProvider::DisconnectFromDatabase()
|
|
{
|
|
TDLOG( TEXT( "TestTrack: %s" ), *FString( __FUNCTION__ ) );
|
|
|
|
// Log the user out of TTP and disconnect from the database
|
|
int32 LogoffResultCode = 0;
|
|
int32 ResultCode = TestTrackSoap->TT1__DatabaseLogoff( IDCookie, LogoffResultCode );
|
|
|
|
if( !VerifyTTPSucceeded( ResultCode ) || !VerifyTTPSucceeded( LogoffResultCode ) )
|
|
{
|
|
// Failed to logoff
|
|
return false;
|
|
}
|
|
|
|
// Great, we logged off successfully.
|
|
TDLOG( TEXT( "TestTrack: Logged out" ) );
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
/**
|
|
* Retrieves a list of filters from the database
|
|
*
|
|
* @param OutFilterNames List filters names
|
|
*
|
|
* @return Returns true if successful
|
|
*/
|
|
bool FTestTrackProvider::QueryFilters( TArray< FString >& OutFilterNames )
|
|
{
|
|
TDLOG( TEXT( "TestTrack: %s" ), *FString( __FUNCTION__ ) );
|
|
|
|
OutFilterNames.Empty();
|
|
|
|
|
|
TT1__getFilterListResponse TTPFilterList;
|
|
int32 ResultCode = TestTrackSoap->TT1__getFilterList(
|
|
IDCookie, // Cookie
|
|
TTPFilterList ); // Out
|
|
|
|
|
|
if( !VerifyTTPSucceeded( ResultCode ) )
|
|
{
|
|
// Couldn't get filter list!
|
|
return false;
|
|
}
|
|
|
|
|
|
if( TTPFilterList.pFilterList != NULL )
|
|
{
|
|
const int32 FilterCount = TTPFilterList.pFilterList->__size;
|
|
|
|
// Pre-size the destination array
|
|
OutFilterNames.Empty( FilterCount );
|
|
|
|
for( int CurFilterIndex = 0; CurFilterIndex < FilterCount; ++CurFilterIndex )
|
|
{
|
|
OutFilterNames.Add( TTPFilterList.pFilterList->__ptritem[ CurFilterIndex ]->name );
|
|
}
|
|
}
|
|
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
/**
|
|
* Retrieves a list of tasks from the database that matches the specified filter name
|
|
*
|
|
* @param InFilterName Filter name to restrict the request by
|
|
* @param OutTaskList List of downloaded tasks
|
|
*
|
|
* @return Returns true if successful
|
|
*/
|
|
bool FTestTrackProvider::QueryTasks( const FString& InFilterName, TArray< FTaskDatabaseEntry >& OutTaskList )
|
|
{
|
|
TDLOG( TEXT( "TestTrack: %s" ), *FString( __FUNCTION__ ) );
|
|
|
|
OutTaskList.Empty();
|
|
|
|
|
|
// Figure out which column indices we're interested in
|
|
uint32 ColumnIndex_Number = INDEX_NONE;
|
|
uint32 ColumnIndex_Priority = INDEX_NONE;
|
|
uint32 ColumnIndex_Name = INDEX_NONE;
|
|
uint32 ColumnIndex_AssignedTo = INDEX_NONE;
|
|
uint32 ColumnIndex_Status = INDEX_NONE;
|
|
uint32 ColumnIndex_CreatedBy = INDEX_NONE;
|
|
|
|
// Setup list of database columns we're interested in retrieving
|
|
TT1ArrayOfCTableColumn TTPTableColumnList;
|
|
{
|
|
int32 NextColumnIndex = 0;
|
|
|
|
TTPTableColumnList.__size = 6;
|
|
TTPTableColumnList.__ptritem = ( TT1__CTableColumn** )soap_malloc( TestTrackSoap->soap, TTPTableColumnList.__size * sizeof( TT1__CTableColumn* ) );
|
|
|
|
// Number
|
|
ColumnIndex_Number = NextColumnIndex;
|
|
TTPTableColumnList.__ptritem[ NextColumnIndex ] = soap_new_TT1__CTableColumn( TestTrackSoap->soap, INDEX_NONE );
|
|
TTPTableColumnList.__ptritem[ NextColumnIndex ]->name = "Number";
|
|
++NextColumnIndex;
|
|
|
|
// Priority
|
|
ColumnIndex_Priority = NextColumnIndex;
|
|
TTPTableColumnList.__ptritem[ NextColumnIndex ] = soap_new_TT1__CTableColumn( TestTrackSoap->soap, INDEX_NONE );
|
|
TTPTableColumnList.__ptritem[ NextColumnIndex ]->name = "Priority";
|
|
++NextColumnIndex;
|
|
|
|
// Name
|
|
ColumnIndex_Name = NextColumnIndex;
|
|
TTPTableColumnList.__ptritem[ NextColumnIndex ] = soap_new_TT1__CTableColumn( TestTrackSoap->soap, INDEX_NONE );
|
|
TTPTableColumnList.__ptritem[ NextColumnIndex ]->name = "Summary";
|
|
++NextColumnIndex;
|
|
|
|
// AssignedTo
|
|
ColumnIndex_AssignedTo = NextColumnIndex;
|
|
TTPTableColumnList.__ptritem[ NextColumnIndex ] = soap_new_TT1__CTableColumn( TestTrackSoap->soap, INDEX_NONE );
|
|
TTPTableColumnList.__ptritem[ NextColumnIndex ]->name = "Currently Assigned To";
|
|
++NextColumnIndex;
|
|
|
|
// Status
|
|
ColumnIndex_Status = NextColumnIndex;
|
|
TTPTableColumnList.__ptritem[ NextColumnIndex ] = soap_new_TT1__CTableColumn( TestTrackSoap->soap, INDEX_NONE );
|
|
TTPTableColumnList.__ptritem[ NextColumnIndex ]->name = "Status";
|
|
++NextColumnIndex;
|
|
|
|
// Created By
|
|
ColumnIndex_CreatedBy = NextColumnIndex;
|
|
TTPTableColumnList.__ptritem[ NextColumnIndex ] = soap_new_TT1__CTableColumn( TestTrackSoap->soap, INDEX_NONE );
|
|
TTPTableColumnList.__ptritem[ NextColumnIndex ]->name = "Created By"; // Entered by
|
|
++NextColumnIndex;
|
|
|
|
check( NextColumnIndex == TTPTableColumnList.__size );
|
|
}
|
|
|
|
|
|
// TTP stores defects in the 'Defect' table
|
|
// For now, we only care about the "Defect" table. Other tables exist such as "User",
|
|
// "Customer", "Task", "Links", "Folder", etc.
|
|
ANSICHAR* ANSIDefectsTableName = "Defect";
|
|
|
|
auto ANSIFilterName = StringCast<ANSICHAR>( *InFilterName );
|
|
|
|
// @todo: Sometimes this returns 'Session busy with prior request' errors
|
|
TT1__getRecordListForTableResponse TTPRecordList;
|
|
int32 ResultCode = TestTrackSoap->TT1__getRecordListForTable(
|
|
IDCookie, // Cookie
|
|
ANSIDefectsTableName, // Table name
|
|
(char*)ANSIFilterName.Get(), // Filter name
|
|
&TTPTableColumnList, // Optional table column list
|
|
TTPRecordList ); // Out
|
|
|
|
|
|
if( !VerifyTTPSucceeded( ResultCode ) )
|
|
{
|
|
// Couldn't get record list!
|
|
return false;
|
|
}
|
|
|
|
|
|
|
|
if( TTPRecordList.recordlist != NULL &&
|
|
TTPRecordList.recordlist->records != NULL )
|
|
{
|
|
const int32 RecordCount = TTPRecordList.recordlist->records->__size;
|
|
|
|
// Pre-size the destination array
|
|
OutTaskList.Empty( RecordCount );
|
|
|
|
for( int CurRecordIndex = 0; CurRecordIndex < RecordCount; ++CurRecordIndex )
|
|
{
|
|
FTaskDatabaseEntry NewTaskEntry;
|
|
|
|
// Number
|
|
if( ColumnIndex_Number != INDEX_NONE )
|
|
{
|
|
const TT1__CRecordData& TTPRecordDataForColumn =
|
|
*TTPRecordList.recordlist->records->__ptritem[ CurRecordIndex ]->row->__ptritem[ ColumnIndex_Number ];
|
|
if( TTPRecordDataForColumn.value != NULL )
|
|
{
|
|
NewTaskEntry.Number = FCString::Atoi( ANSI_TO_TCHAR( TTPRecordDataForColumn.value ) );
|
|
}
|
|
}
|
|
|
|
// Priority
|
|
if( ColumnIndex_Priority != INDEX_NONE )
|
|
{
|
|
const TT1__CRecordData& TTPRecordDataForColumn =
|
|
*TTPRecordList.recordlist->records->__ptritem[ CurRecordIndex ]->row->__ptritem[ ColumnIndex_Priority ];
|
|
if( TTPRecordDataForColumn.value != NULL )
|
|
{
|
|
NewTaskEntry.Priority = TTPRecordDataForColumn.value;
|
|
}
|
|
}
|
|
|
|
// Name
|
|
if( ColumnIndex_Name != INDEX_NONE )
|
|
{
|
|
const TT1__CRecordData& TTPRecordDataForColumn =
|
|
*TTPRecordList.recordlist->records->__ptritem[ CurRecordIndex ]->row->__ptritem[ ColumnIndex_Name ];
|
|
if( TTPRecordDataForColumn.value != NULL )
|
|
{
|
|
NewTaskEntry.Summary = TTPRecordDataForColumn.value;
|
|
|
|
// @todo: For whatever reason, TestTrack seems to crop task names to 126 characters and then
|
|
// insert &* characters afterwards. We'll clean up the text here.
|
|
if( NewTaskEntry.Summary.Len() > 126 )
|
|
{
|
|
NewTaskEntry.Summary = NewTaskEntry.Summary.Left( 126 );
|
|
}
|
|
}
|
|
}
|
|
|
|
// AssignedTo
|
|
if( ColumnIndex_AssignedTo != INDEX_NONE )
|
|
{
|
|
const TT1__CRecordData& TTPRecordDataForColumn =
|
|
*TTPRecordList.recordlist->records->__ptritem[ CurRecordIndex ]->row->__ptritem[ ColumnIndex_AssignedTo ];
|
|
if( TTPRecordDataForColumn.value != NULL )
|
|
{
|
|
NewTaskEntry.AssignedTo = TTPRecordDataForColumn.value;
|
|
}
|
|
}
|
|
|
|
// Status
|
|
if( ColumnIndex_Status != INDEX_NONE )
|
|
{
|
|
const TT1__CRecordData& TTPRecordDataForColumn =
|
|
*TTPRecordList.recordlist->records->__ptritem[ CurRecordIndex ]->row->__ptritem[ ColumnIndex_Status ];
|
|
if( TTPRecordDataForColumn.value != NULL )
|
|
{
|
|
NewTaskEntry.Status = TTPRecordDataForColumn.value;
|
|
}
|
|
}
|
|
|
|
// CreatedBy
|
|
if( ColumnIndex_CreatedBy != INDEX_NONE )
|
|
{
|
|
const TT1__CRecordData& TTPRecordDataForColumn =
|
|
*TTPRecordList.recordlist->records->__ptritem[ CurRecordIndex ]->row->__ptritem[ ColumnIndex_CreatedBy ];
|
|
if( TTPRecordDataForColumn.value != NULL )
|
|
{
|
|
NewTaskEntry.CreatedBy = TTPRecordDataForColumn.value;
|
|
}
|
|
}
|
|
|
|
OutTaskList.Add( NewTaskEntry );
|
|
}
|
|
}
|
|
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
/**
|
|
* Retrieves details about a specific task from the database
|
|
*
|
|
* @param InNumber Task number
|
|
* @param OutDetails [Out] Details for the requested task
|
|
*
|
|
* @return Returns true if successful
|
|
*/
|
|
bool FTestTrackProvider::QueryTaskDetails( const uint32 InNumber, FTaskDatabaseEntryDetails& OutDetails )
|
|
{
|
|
TDLOG( TEXT( "TestTrack: %s" ), *FString( __FUNCTION__ ) );
|
|
|
|
// Reset output
|
|
OutDetails = FTaskDatabaseEntryDetails();
|
|
|
|
|
|
// Query defect info
|
|
TT1__getDefectResponse TTPDefect;
|
|
int32 ResultCode = TestTrackSoap->TT1__getDefect(
|
|
IDCookie, // Cookie
|
|
InNumber, // Defect number
|
|
NULL, // Summary string (optional)
|
|
false, // Download attachments?
|
|
TTPDefect ); // Out
|
|
|
|
|
|
if( !VerifyTTPSucceeded( ResultCode ) )
|
|
{
|
|
// Couldn't get defect info!
|
|
return false;
|
|
}
|
|
|
|
|
|
OutDetails.Number = *TTPDefect.pDefect->defectnumber;
|
|
OutDetails.Summary = TTPDefect.pDefect->summary;
|
|
OutDetails.Priority = TTPDefect.pDefect->priority;
|
|
OutDetails.Status = TTPDefect.pDefect->state;
|
|
OutDetails.CreatedBy = TTPDefect.pDefect->createdbyuser;
|
|
|
|
|
|
// @todo: Can we use bold text here to make things look nicer?
|
|
// -> We're already using a rich text control, so it seems likely
|
|
|
|
// @todo: Display date created, date modified, event time/dates, other fields, etc.
|
|
|
|
OutDetails.Description = TEXT( "\n" );
|
|
|
|
if( TTPDefect.pDefect->reportedbylist != NULL )
|
|
{
|
|
for( int32 CurReportIndex = 0; CurReportIndex < TTPDefect.pDefect->reportedbylist->__size; ++CurReportIndex )
|
|
{
|
|
OutDetails.Description += FString( TTPDefect.pDefect->reportedbylist->__ptritem[ CurReportIndex ]->comments );
|
|
OutDetails.Description += TEXT( "\n\n" );
|
|
}
|
|
}
|
|
|
|
if( TTPDefect.pDefect->eventlist != NULL )
|
|
{
|
|
for( int32 CurReportIndex = 0; CurReportIndex < TTPDefect.pDefect->eventlist->__size; ++CurReportIndex )
|
|
{
|
|
// @todo: Skip display of certain event types? (Auto assignments and such)
|
|
|
|
OutDetails.Description += TEXT( "\n-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-\n" );
|
|
|
|
OutDetails.Description +=
|
|
FString::Printf(
|
|
TEXT( "\n%s Event by %s (%s):\n\n" ),
|
|
*FString( TTPDefect.pDefect->eventlist->__ptritem[ CurReportIndex ]->name ),
|
|
*FString( TTPDefect.pDefect->eventlist->__ptritem[ CurReportIndex ]->user ),
|
|
*FString( TTPDefect.pDefect->eventlist->__ptritem[ CurReportIndex ]->generatedeventtype ) );
|
|
|
|
OutDetails.Description += FString( TTPDefect.pDefect->eventlist->__ptritem[ CurReportIndex ]->notes );
|
|
OutDetails.Description += TEXT( "\n" );
|
|
}
|
|
}
|
|
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
/**
|
|
* Marks the specified task as complete
|
|
*
|
|
* @param InNumber Task number
|
|
* @param InResolutionData Resolution data for this task
|
|
*
|
|
* @return Returns true if successful
|
|
*/
|
|
bool FTestTrackProvider::MarkTaskComplete( const uint32 InNumber, const FTaskResolutionData& InResolutionData )
|
|
{
|
|
TDLOG( TEXT( "TestTrack: %s" ), *FString( __FUNCTION__ ) );
|
|
|
|
|
|
// Open the defect for editing
|
|
TT1__editDefectResponse TTPDefect;
|
|
int ResultCode = TestTrackSoap->TT1__editDefect(
|
|
IDCookie, // Cookie
|
|
InNumber, // Defect number
|
|
NULL, // Summary string (optional)
|
|
false, // Download attachments?
|
|
TTPDefect ); // Out
|
|
|
|
if( !VerifyTTPSucceeded( ResultCode ) )
|
|
{
|
|
// Couldn't open defect for editing
|
|
return false;
|
|
}
|
|
|
|
|
|
|
|
// Grab the current date and time
|
|
time_t CurrentDateTime;
|
|
time( &CurrentDateTime );
|
|
|
|
double HoursToComplete = InResolutionData.HoursToComplete;
|
|
|
|
TDLOG( TEXT( "TestTrack: User real name for defect accountability: '%s'" ), *UserRealName );
|
|
|
|
auto ANSIUserRealName = StringCast<ANSICHAR>( *UserRealName );
|
|
|
|
// Change bug status to 'Fixed'
|
|
TTPDefect.pDefect->state = "Fixed";
|
|
TTPDefect.pDefect->datetimemodified = &CurrentDateTime;
|
|
TTPDefect.pDefect->modifiedbyuser = (char*)ANSIUserRealName.Get();
|
|
TTPDefect.pDefect->actualhourstofix = &HoursToComplete;
|
|
|
|
|
|
auto ANSIComments = StringCast<ANSICHAR>( *InResolutionData.Comments );
|
|
auto ANSIResolutionType = StringCast<ANSICHAR>( *InResolutionData.ResolutionType );
|
|
auto ANSIChangelistNumber = StringCast<ANSICHAR>( *FString( FString::FromInt( InResolutionData.ChangelistNumber ) ) );
|
|
|
|
|
|
// Add a 'fix' event
|
|
{
|
|
TT1__CEvent* NewEvent = soap_new_TT1__CEvent( TestTrackSoap->soap, INDEX_NONE );
|
|
{
|
|
NewEvent->user = (char*)ANSIUserRealName.Get();
|
|
NewEvent->notes = (char*)ANSIComments.Get();
|
|
NewEvent->name = "Fix";
|
|
NewEvent->generatedeventtype = "User";
|
|
NewEvent->date = CurrentDateTime;
|
|
NewEvent->hours = &HoursToComplete;
|
|
NewEvent->totaltimespent = &HoursToComplete;
|
|
|
|
|
|
// Fill in custom fields
|
|
{
|
|
NewEvent->fieldlist = soap_new_TT1ArrayOfCField( TestTrackSoap->soap, INDEX_NONE );
|
|
|
|
const uint32 CustomFieldCount = 4;
|
|
NewEvent->fieldlist->__size = CustomFieldCount;
|
|
NewEvent->fieldlist->__ptritem = ( TT1__CField** )soap_malloc( TestTrackSoap->soap, NewEvent->fieldlist->__size * sizeof( TT1__CField* ) );
|
|
uint32 NextCustomFieldIndex = 0;
|
|
|
|
{
|
|
TT1__CBooleanField* NewField = soap_new_TT1__CBooleanField( TestTrackSoap->soap, INDEX_NONE );
|
|
NewField->name = "Affects Documentation";
|
|
NewField->value = false; // @todo: Support this?
|
|
NewEvent->fieldlist->__ptritem[ NextCustomFieldIndex++ ] = NewField;
|
|
}
|
|
|
|
{
|
|
TT1__CBooleanField* NewField = soap_new_TT1__CBooleanField( TestTrackSoap->soap, INDEX_NONE );
|
|
NewField->name = "Affects Test Plan";
|
|
NewField->value = false; // @todo: Support this?
|
|
NewEvent->fieldlist->__ptritem[ NextCustomFieldIndex++ ] = NewField;
|
|
}
|
|
|
|
{
|
|
TT1__CDropdownField* NewField = soap_new_TT1__CDropdownField( TestTrackSoap->soap, INDEX_NONE );
|
|
NewField->name = "Resolution";
|
|
NewField->value = (char*)ANSIResolutionType.Get();
|
|
NewEvent->fieldlist->__ptritem[ NextCustomFieldIndex++ ] = NewField;
|
|
}
|
|
|
|
{
|
|
TT1__CVersionField* NewField = soap_new_TT1__CVersionField( TestTrackSoap->soap, INDEX_NONE );
|
|
NewField->name = "Changelist";
|
|
NewField->value = (char*)ANSIChangelistNumber.Get();
|
|
NewEvent->fieldlist->__ptritem[ NextCustomFieldIndex++ ] = NewField;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
uint32 OldEventCount = 0;
|
|
TT1__CEvent** OldEventList = NULL;
|
|
if( TTPDefect.pDefect->eventlist != NULL )
|
|
{
|
|
OldEventCount = TTPDefect.pDefect->eventlist->__size;
|
|
OldEventList = TTPDefect.pDefect->eventlist->__ptritem;
|
|
}
|
|
else
|
|
{
|
|
// Allocate event list if we need it (should never have to, really)
|
|
TTPDefect.pDefect->eventlist = soap_new_TT1ArrayOfCEvent( TestTrackSoap->soap, INDEX_NONE );
|
|
}
|
|
|
|
// Allocate the new event array
|
|
// NOTE: The old event array is still linked to SOAP and will be automatically cleaned up later
|
|
TTPDefect.pDefect->eventlist->__size = OldEventCount + 1;
|
|
TTPDefect.pDefect->eventlist->__ptritem =
|
|
( TT1__CEvent** )soap_malloc( TestTrackSoap->soap, TTPDefect.pDefect->eventlist->__size * sizeof( TT1__CEvent* ) );
|
|
|
|
// Copy old events over
|
|
for( uint32 CurEventIndex = 0; CurEventIndex < OldEventCount; ++CurEventIndex )
|
|
{
|
|
TTPDefect.pDefect->eventlist->__ptritem[ CurEventIndex ] = OldEventList[ CurEventIndex ];
|
|
}
|
|
|
|
// Add new event to the list
|
|
const uint32 NewEventIndex = OldEventCount;
|
|
TTPDefect.pDefect->eventlist->__ptritem[ NewEventIndex ] = NewEvent;
|
|
}
|
|
|
|
|
|
// @todo: After saving the defect, why is 'AM Fix Priority' (custom field) and 'Estimated Effort'
|
|
// unexpectedly showing up as edited in the bug's history?
|
|
|
|
// Save changes to this defect
|
|
int SaveResultCode = 0;
|
|
ResultCode = TestTrackSoap->TT1__saveDefect(
|
|
IDCookie, // Cookie
|
|
TTPDefect.pDefect, // Defect (with changes to save)
|
|
SaveResultCode ); // Out
|
|
|
|
|
|
if( !VerifyTTPSucceeded( ResultCode ) || !VerifyTTPSucceeded( SaveResultCode ) )
|
|
{
|
|
// Couldn't save defect changes
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
#endif // WITH_TESTTRACK
|