// 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( *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( *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( *InResolutionData.Comments ); auto ANSIResolutionType = StringCast( *InResolutionData.ResolutionType ); auto ANSIChangelistNumber = StringCast( *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