Files
UnrealEngineUWP/Engine/Source/Editor/TaskBrowser/Private/TestTrackTaskDatabaseProvider.cpp
Max Preussner 3aece47882 Docs: Removed file comments and added missing code documentation
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]
2014-06-12 23:22:18 -04:00

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