From: Colin Finck Date: Mon, 5 Jan 2009 12:41:34 +0000 (+0000) Subject: Introducing the "ReactOS Automatic Testing Utility", superseding our current syssetup... X-Git-Tag: ReactOS-0.3.8~533 X-Git-Url: https://git.reactos.org/?p=reactos.git;a=commitdiff_plain;h=d2148478c62687a7c73214d8197f719701baeb84 Introducing the "ReactOS Automatic Testing Utility", superseding our current syssetup/cmd/dbgprint hack for running automatic regression tests. Without any parameters, it mostly works the same as our current solution, but all in a standalone application. Adding the /w parameter will submit all results to the web service committed in my previous commit. The application would also make it possible to run Wine Tests regularly on a Windows machine and submitting the results. This would make sure that all Wine tests also pass under Windows. svn path=/trunk/; revision=38580 --- diff --git a/rostests/directory.rbuild b/rostests/directory.rbuild index bbd656ebcf4..5dafb2d3edf 100644 --- a/rostests/directory.rbuild +++ b/rostests/directory.rbuild @@ -13,6 +13,9 @@ + + + diff --git a/rostests/rosautotest/main.c b/rostests/rosautotest/main.c new file mode 100644 index 00000000000..31c984c536b --- /dev/null +++ b/rostests/rosautotest/main.c @@ -0,0 +1,326 @@ +/* + * PROJECT: ReactOS Automatic Testing Utility + * LICENSE: GNU GPLv2 or any later version as published by the Free Software Foundation + * PURPOSE: Main implementation file + * COPYRIGHT: Copyright 2008-2009 Colin Finck + */ + +#include "precomp.h" + +typedef void (WINAPI *GETSYSINFO)(LPSYSTEM_INFO); + +APP_OPTIONS AppOptions = {0}; +HANDLE hProcessHeap; +PCHAR AuthenticationRequestString = NULL; +PCHAR SystemInfoRequestString = NULL; + +/** + * Gets a value from a specified INI file and returns it converted to ASCII. + * + * @param AppName + * The INI section to look in (lpAppName parameter passed to GetPrivateProfileStringW) + * + * @param KeyName + * The key to look for in the specified section (lpKeyName parameter passed to GetPrivateProfileStringW) + * + * @param FileName + * The path to the INI file + * + * @param ReturnedValue + * Pointer to a CHAR pointer, which will receive the read and converted value. + * The caller needs to HeapFree that value manually. + * + * @return + * Returns the string length of the read value (in characters) or zero if we didn't get any value. + */ +static DWORD +IntGetINIValueA(PCWCH AppName, PCWCH KeyName, PCWCH FileName, char** ReturnedValue) +{ + DWORD Length; + WCHAR Buffer[2048]; + + /* Load the value into a temporary Unicode buffer */ + Length = GetPrivateProfileStringW(AppName, KeyName, NULL, Buffer, sizeof(Buffer) / sizeof(WCHAR), FileName); + + if(!Length) + return 0; + + /* Convert the string to ANSI charset */ + *ReturnedValue = HeapAlloc(hProcessHeap, 0, Length + 1); + WideCharToMultiByte(CP_ACP, 0, Buffer, Length + 1, *ReturnedValue, Length + 1, NULL, NULL); + + return Length; +} + +/** + * Gets the username and password hash from the "rosautotest.ini" file if the user enabled submitting the results to the web service. + * The "rosautotest.ini" file should look like this: + * + * [Login] + * UserName=TestMan + * PasswordHash=1234567890abcdef1234567890abcdef + */ +static BOOL +IntGetConfigurationValues() +{ + const CHAR PasswordHashProp[] = "&passwordhash="; + const CHAR UserNameProp[] = "&username="; + + DWORD DataLength; + DWORD Length; + PCHAR PasswordHash; + PCHAR UserName; + WCHAR ConfigFile[MAX_PATH]; + + /* We only need this if the results are going to be submitted */ + if(!AppOptions.Submit) + return TRUE; + + /* Build the path to the configuration file */ + Length = GetWindowsDirectoryW(ConfigFile, MAX_PATH); + wcscpy(&ConfigFile[Length], L"\\rosautotest.ini"); + + /* Check if it exists */ + if(GetFileAttributesW(ConfigFile) == INVALID_FILE_ATTRIBUTES) + return FALSE; + + /* Get the required length of the authentication request string */ + DataLength = sizeof(UserNameProp) - 1; + Length = IntGetINIValueA(L"Login", L"UserName", ConfigFile, &UserName); + + if(!Length) + { + StringOut("UserName is missing in the configuration file\n"); + return FALSE; + } + + /* Some characters might need to be escaped and an escaped character takes 3 bytes */ + DataLength += 3 * Length; + + DataLength += sizeof(PasswordHashProp) - 1; + Length = IntGetINIValueA(L"Login", L"PasswordHash", ConfigFile, &PasswordHash); + + if(!Length) + { + StringOut("PasswordHash is missing in the configuration file\n"); + return FALSE; + } + + DataLength += 3 * Length; + + /* Build the string */ + AuthenticationRequestString = HeapAlloc(hProcessHeap, 0, DataLength + 1); + + strcpy(AuthenticationRequestString, UserNameProp); + EscapeString(&AuthenticationRequestString[strlen(AuthenticationRequestString)], UserName); + + strcat(AuthenticationRequestString, PasswordHashProp); + EscapeString(&AuthenticationRequestString[strlen(AuthenticationRequestString)], PasswordHash); + + return TRUE; +} + +/** + * Determines on which platform we're running on. + * Prepares the appropriate request strings needed if we want to submit test results to the web service. + */ +static BOOL +IntGetBuildAndPlatform() +{ + const CHAR PlatformProp[] = "&platform="; + const CHAR RevisionProp[] = "&revision="; + + CHAR BuildNo[BUILDNO_LENGTH]; + CHAR Platform[PLATFORM_LENGTH]; + CHAR PlatformArchitecture[3]; + CHAR ProductType; + DWORD DataLength; + GETSYSINFO GetSysInfo; + HANDLE hKernel32; + OSVERSIONINFOEXW os; + SYSTEM_INFO si; + WCHAR WindowsDirectory[MAX_PATH]; + + /* Get the build from the define */ + _ultoa(KERNEL_VERSION_BUILD_HEX, BuildNo, 10); + + /* Check if we are running under ReactOS from the SystemRoot directory */ + GetWindowsDirectoryW(WindowsDirectory, MAX_PATH); + + if(!_wcsnicmp(&WindowsDirectory[3], L"reactos", 7)) + { + /* Yes, we are most-probably under ReactOS */ + strcpy(Platform, "reactos"); + } + else + { + /* No, then use the info from GetVersionExW */ + os.dwOSVersionInfoSize = sizeof(os); + + if(!GetVersionExW((LPOSVERSIONINFOW)&os)) + { + StringOut("GetVersionExW failed\n"); + return FALSE; + } + + if(os.dwMajorVersion < 5) + { + StringOut("Application requires at least Windows 2000!\n"); + return FALSE; + } + + if(os.wProductType == VER_NT_WORKSTATION) + ProductType = 'w'; + else + ProductType = 's'; + + /* Print all necessary identification information into the Platform string */ + sprintf(Platform, "%lu.%lu.%lu.%u.%u.%c", os.dwMajorVersion, os.dwMinorVersion, os.dwBuildNumber, os.wServicePackMajor, os.wServicePackMinor, ProductType); + } + + /* We also need to know about the processor architecture. + To retrieve this information accurately, check whether "GetNativeSystemInfo" is exported and use it then, otherwise fall back to "GetSystemInfo". */ + hKernel32 = GetModuleHandleW(L"KERNEL32.DLL"); + GetSysInfo = (GETSYSINFO)GetProcAddress(hKernel32, "GetNativeSystemInfo"); + + if(!GetSysInfo) + GetSysInfo = (GETSYSINFO)GetProcAddress(hKernel32, "GetSystemInfo"); + + GetSysInfo(&si); + + PlatformArchitecture[0] = '.'; + _ultoa(si.wProcessorArchitecture, &PlatformArchitecture[1], 10); + PlatformArchitecture[2] = 0; + strcat(Platform, PlatformArchitecture); + + /* Get the required length of the system info request string */ + DataLength = sizeof(RevisionProp) - 1; + DataLength += strlen(BuildNo); + DataLength += sizeof(PlatformProp) - 1; + DataLength += strlen(Platform); + + /* Now build the string */ + SystemInfoRequestString = HeapAlloc(hProcessHeap, 0, DataLength + 1); + strcpy(SystemInfoRequestString, RevisionProp); + strcat(SystemInfoRequestString, BuildNo); + strcat(SystemInfoRequestString, PlatformProp); + strcat(SystemInfoRequestString, Platform); + + return TRUE; +} + +/** + * Prints the application usage. + */ +static VOID +IntPrintUsage() +{ + printf("rosautotest - ReactOS Automatic Testing Utility\n"); + printf("Usage: rosautotest [options] [module] [test]\n"); + printf(" options:\n"); + printf(" /? - Shows this help\n"); + printf(" /s - Shut down the system after finishing the tests\n"); + printf(" /w - Submit the results to the webservice\n"); + printf(" Requires a \"rosautotest.ini\" with valid login data.\n"); + printf("\n"); + printf(" module:\n"); + printf(" The module to be tested (i.e. \"advapi32\")\n"); + printf(" If this parameter is specified without any test parameter,\n"); + printf(" all tests of the specified module are run.\n"); + printf("\n"); + printf(" test:\n"); + printf(" The test to be run. Needs to be a test of the specified module.\n"); +} + +/** + * Main entry point + */ +int +wmain(int argc, wchar_t* argv[]) +{ + int Result = 0; + UINT i; + + hProcessHeap = GetProcessHeap(); + + /* Parse the command line arguments */ + for(i = 1; i < (UINT)argc; i++) + { + if(argv[i][0] == '-' || argv[i][0] == '/') + { + switch(argv[i][1]) + { + case 's': + AppOptions.Shutdown = TRUE; + break; + + case 'w': + AppOptions.Submit = TRUE; + break; + + default: + Result = 1; + /* Fall through */ + + case '?': + IntPrintUsage(); + goto End; + } + } + else + { + size_t Length; + + /* Which parameter is this? */ + if(!AppOptions.Module) + { + /* Copy the parameter */ + Length = (wcslen(argv[i]) + 1) * sizeof(WCHAR); + AppOptions.Module = HeapAlloc(hProcessHeap, 0, Length); + memcpy(AppOptions.Module, argv[i], Length); + } + else if(!AppOptions.Test) + { + /* Copy the parameter converted to ASCII */ + Length = WideCharToMultiByte(CP_ACP, 0, argv[i], -1, NULL, 0, NULL, NULL); + AppOptions.Test = HeapAlloc(hProcessHeap, 0, Length); + WideCharToMultiByte(CP_ACP, 0, argv[i], -1, AppOptions.Test, Length, NULL, NULL); + } + else + { + Result = 1; + IntPrintUsage(); + goto End; + } + } + } + + if(!IntGetConfigurationValues() || !IntGetBuildAndPlatform() || !RunWineTests()) + { + Result = 1; + goto End; + } + + /* For sysreg */ + OutputDebugStringA("SYSREG_CHECKPOINT:THIRDBOOT_COMPLETE\n"); + +End: + /* Cleanup */ + if(AppOptions.Module) + HeapFree(hProcessHeap, 0, AppOptions.Module); + + if(AppOptions.Test) + HeapFree(hProcessHeap, 0, AppOptions.Test); + + if(AuthenticationRequestString) + HeapFree(hProcessHeap, 0, AuthenticationRequestString); + + if(SystemInfoRequestString) + HeapFree(hProcessHeap, 0, SystemInfoRequestString); + + /* Shut down the system if requested */ + if(AppOptions.Shutdown && !ShutdownSystem()) + Result = 1; + + return Result; +} diff --git a/rostests/rosautotest/precomp.h b/rostests/rosautotest/precomp.h new file mode 100644 index 00000000000..87fdf2d7c9d --- /dev/null +++ b/rostests/rosautotest/precomp.h @@ -0,0 +1,81 @@ +/* Includes */ +#include + +#include +#include +#include + +#include + +/* Defines */ +#define BUFFER_BLOCKSIZE 2048 +#define BUILDNO_LENGTH 10 +#define PLATFORM_LENGTH 25 +#define SERVER_HOSTNAME L"localhost" +#define SERVER_FILE L"testman/webservice/" + +/* Enums */ +typedef enum _TESTTYPES +{ + WineTest +} +TESTTYPES; + +/* Structs */ +typedef struct _APP_OPTIONS +{ + BOOL Shutdown; + BOOL Submit; + PWSTR Module; + PCHAR Test; +} +APP_OPTIONS, *PAPP_OPTIONS; + +typedef struct _WINE_GETSUITEID_DATA +{ + PCHAR Module; + PCHAR Test; +} +WINE_GETSUITEID_DATA, *PWINE_GETSUITEID_DATA; + +typedef struct _GENERAL_SUBMIT_DATA +{ + PCHAR TestID; + PCHAR SuiteID; +} +GENERAL_SUBMIT_DATA, *PGENERAL_SUBMIT_DATA; + +typedef struct _WINE_SUBMIT_DATA +{ + GENERAL_SUBMIT_DATA General; + PCHAR Log; +} +WINE_SUBMIT_DATA, *PWINE_SUBMIT_DATA; + +typedef struct _GENERAL_FINISH_DATA +{ + PCHAR TestID; +} +GENERAL_FINISH_DATA, *PGENERAL_FINISH_DATA; + +/* main.c */ +extern APP_OPTIONS AppOptions; +extern HANDLE hProcessHeap; +extern PCHAR AuthenticationRequestString; +extern PCHAR SystemInfoRequestString; + +/* shutdown.c */ +BOOL ShutdownSystem(); + +/* tools.c */ +VOID EscapeString(PCHAR Output, PCHAR Input); +VOID StringOut(PCHAR String); + +/* webservice.c */ +PCHAR GetTestID(TESTTYPES TestType); +PCHAR GetSuiteID(TESTTYPES TestType, const PVOID TestData); +BOOL Submit(TESTTYPES TestType, const PVOID TestData); +BOOL Finish(TESTTYPES TestType, const PVOID TestData); + +/* winetests.c */ +BOOL RunWineTests(); diff --git a/rostests/rosautotest/rosautotest.rbuild b/rostests/rosautotest/rosautotest.rbuild new file mode 100644 index 00000000000..b372227fc8a --- /dev/null +++ b/rostests/rosautotest/rosautotest.rbuild @@ -0,0 +1,15 @@ + + + + . + advapi32 + kernel32 + user32 + wininet + main.c + shutdown.c + tools.c + webservice.c + winetests.c + precomp.h + diff --git a/rostests/rosautotest/shutdown.c b/rostests/rosautotest/shutdown.c new file mode 100644 index 00000000000..7493d7962f7 --- /dev/null +++ b/rostests/rosautotest/shutdown.c @@ -0,0 +1,52 @@ +/* + * PROJECT: ReactOS Automatic Testing Utility + * LICENSE: GNU GPLv2 or any later version as published by the Free Software Foundation + * PURPOSE: Helper function for shutting down the system + * COPYRIGHT: Copyright 2008-2009 Colin Finck + */ + +#include "precomp.h" + +/** + * Shuts down the system. + * + * @return + * TRUE if everything went well, FALSE if there was a problem while trying to shut down the system. + */ +BOOL ShutdownSystem() +{ + HANDLE hToken; + TOKEN_PRIVILEGES Privileges; + + if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hToken)) + { + StringOut("OpenProcessToken failed\n"); + return FALSE; + } + + /* Get the LUID for the Shutdown privilege */ + if (!LookupPrivilegeValueW(NULL, SE_SHUTDOWN_NAME, &Privileges.Privileges[0].Luid)) + { + StringOut("LookupPrivilegeValue failed\n"); + return FALSE; + } + + /* Assign the Shutdown privilege to our process */ + Privileges.PrivilegeCount = 1; + Privileges.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; + + if (!AdjustTokenPrivileges(hToken, FALSE, &Privileges, 0, NULL, NULL)) + { + StringOut("AdjustTokenPrivileges failed\n"); + return FALSE; + } + + /* Finally shut down the system */ + if(!ExitWindowsEx(EWX_POWEROFF, SHTDN_REASON_MAJOR_OTHER | SHTDN_REASON_MINOR_OTHER | SHTDN_REASON_FLAG_PLANNED)) + { + StringOut("ExitWindowsEx failed\n"); + return FALSE; + } + + return TRUE; +} diff --git a/rostests/rosautotest/tools.c b/rostests/rosautotest/tools.c new file mode 100644 index 00000000000..763c817c7f4 --- /dev/null +++ b/rostests/rosautotest/tools.c @@ -0,0 +1,57 @@ +/* + * PROJECT: ReactOS Automatic Testing Utility + * LICENSE: GNU GPLv2 or any later version as published by the Free Software Foundation + * PURPOSE: Various helper functions + * COPYRIGHT: Copyright 2008-2009 Colin Finck + */ + +#include "precomp.h" + +/** + * Escapes a string according to RFC 1738. + * Required for passing parameters to the web service. + * + * @param Output + * Pointer to a CHAR array, which will receive the escaped string. + * The output buffer must be large enough to hold the full escaped string. You're on the safe side + * if you make the output buffer three times as large as the input buffer. + * + * @param Input + * Pointer to a CHAR array, which contains the input buffer to escape. + */ +VOID +EscapeString(PCHAR Output, PCHAR Input) +{ + const CHAR HexCharacters[] = "0123456789ABCDEF"; + + do + { + if(strchr("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~", *Input) ) + { + /* It's a character we don't need to escape, just add it to the output string */ + *Output++ = *Input; + } + else + { + /* We need to escape this character */ + *Output++ = '%'; + *Output++ = HexCharacters[((UCHAR)*Input >> 4) % 16]; + *Output++ = HexCharacters[(UCHAR)*Input % 16]; + } + } while(*++Input); + + *Output = 0; +} + +/** + * Outputs a string through the standard output and the debug output. + * + * @param String + * The string to output + */ +VOID +StringOut(PCHAR String) +{ + printf(String); + OutputDebugStringA(String); +} diff --git a/rostests/rosautotest/webservice.c b/rostests/rosautotest/webservice.c new file mode 100644 index 00000000000..dcfd79fa51f --- /dev/null +++ b/rostests/rosautotest/webservice.c @@ -0,0 +1,417 @@ +/* + * PROJECT: ReactOS Automatic Testing Utility + * LICENSE: GNU GPLv2 or any later version as published by the Free Software Foundation + * PURPOSE: Submitting test results to the Web Service + * COPYRIGHT: Copyright 2008-2009 Colin Finck + */ + +#include "precomp.h" + +static const CHAR ActionProp[] = "action="; +static const CHAR TestIDProp[] = "&testid="; +static const CHAR TestTypeProp[] = "&testtype="; +static const CHAR WineTestType[] = "wine"; + +/** + * Sends data to the ReactOS Web Test Manager web service. + * + * @param Data + * Pointer to a CHAR pointer, which contains the data to submit as HTTP POST data. + * The buffer behind this pointer had to be allocated with HeapAlloc. + * Returns the data received by the web service after the call. + * + * @param DataLength + * Pointer to a DWORD, which contains the length of the data to submit (in bytes). + * Returns the length of the data received by the web service after the call (in bytes). + * + * @return + * TRUE if everything went well, FALSE if an error occured while submitting the request. + * In case of an error, the function will output an appropriate error message through StringOut. + */ +static BOOL +IntDoRequest(char** Data, PDWORD DataLength) +{ + const WCHAR Headers[] = L"Content-Type: application/x-www-form-urlencoded"; + + HINTERNET hHTTP; + HINTERNET hHTTPRequest; + HINTERNET hInet; + + /* Establish an internet connection to the "testman" server */ + hInet = InternetOpenW(L"rosautotest", INTERNET_OPEN_TYPE_PRECONFIG, NULL, NULL, 0); + + if(!hInet) + { + StringOut("InternetOpenW failed\n"); + return FALSE; + } + + hHTTP = InternetConnectW(hInet, SERVER_HOSTNAME, INTERNET_DEFAULT_HTTP_PORT, NULL, NULL, INTERNET_SERVICE_HTTP, 0, 0); + + if(!hHTTP) + { + StringOut("InternetConnectW failed\n"); + return FALSE; + } + + /* Post our test results to the web service */ + hHTTPRequest = HttpOpenRequestW(hHTTP, L"POST", SERVER_FILE, NULL, NULL, NULL, INTERNET_FLAG_NO_COOKIES | INTERNET_FLAG_RELOAD | INTERNET_FLAG_NO_CACHE_WRITE, 0); + + if(!hHTTPRequest) + { + StringOut("HttpOpenRequestW failed\n"); + return FALSE; + } + + if(!HttpSendRequestW(hHTTPRequest, Headers, wcslen(Headers), *Data, *DataLength)) + { + StringOut("HttpSendRequestW failed\n"); + return FALSE; + } + + HeapFree(hProcessHeap, 0, *Data); + + /* Get the response */ + if(!InternetQueryDataAvailable(hHTTPRequest, DataLength, 0, 0)) + { + StringOut("InternetQueryDataAvailable failed\n"); + return FALSE; + } + + *Data = HeapAlloc(hProcessHeap, 0, *DataLength + 1); + + if(!InternetReadFile(hHTTPRequest, *Data, *DataLength, DataLength)) + { + StringOut("InternetReadFile failed\n"); + return FALSE; + } + + (*Data)[*DataLength] = 0; + + InternetCloseHandle(hHTTPRequest); + InternetCloseHandle(hHTTP); + InternetCloseHandle(hInet); + + return TRUE; +} + +/** + * Determines whether a string contains entirely numeric values. + * + * @param Input + * The string to check. + * + * @return + * TRUE if the string is entirely numeric, FALSE otherwise. + */ +static BOOL +IsNumber(PCHAR Input) +{ + do + { + if(!isdigit(*Input)) + return FALSE; + + ++Input; + } + while(*Input); + + return TRUE; +} + +/** + * Requests a Test ID from the web service for our test run. + * + * @param TestType + * Value from the TESTTYPES enum indicating the type of test we are about to submit. + * + * @return + * Returns the Test ID as a CHAR array if successful or NULL otherwise. + */ +PCHAR +GetTestID(TESTTYPES TestType) +{ + const CHAR GetTestIDAction[] = "gettestid"; + + DWORD DataLength; + PCHAR Data; + + /* Build the full request string */ + DataLength = sizeof(ActionProp) - 1 + sizeof(GetTestIDAction) - 1; + DataLength += strlen(AuthenticationRequestString) + strlen(SystemInfoRequestString); + DataLength += sizeof(TestTypeProp) - 1; + + switch(TestType) + { + case WineTest: + DataLength += sizeof(WineTestType) - 1; + break; + } + + Data = HeapAlloc(hProcessHeap, 0, DataLength + 1); + strcpy(Data, ActionProp); + strcat(Data, GetTestIDAction); + strcat(Data, AuthenticationRequestString); + strcat(Data, SystemInfoRequestString); + strcat(Data, TestTypeProp); + + switch(TestType) + { + case WineTest: + strcat(Data, WineTestType); + break; + } + + if(!IntDoRequest(&Data, &DataLength)) + return NULL; + + /* Verify that this is really a number */ + if(!IsNumber(Data)) + { + StringOut("Expected Test ID, but received:\n"); + StringOut(Data); + HeapFree(hProcessHeap, 0, Data); + return NULL; + } + + return Data; +} + +/** + * Requests a Suite ID from the web service for our module/test combination. + * + * @param TestType + * Value from the TESTTYPES enum indicating the type of test we are about to submit. + * + * @param TestData + * Pointer to a *_GETSUITEID_DATA structure appropriate for our selected test type. + * Contains other input information for this request. + * + * @return + * Returns the Suite ID as a CHAR array if successful or NULL otherwise. + */ +PCHAR +GetSuiteID(TESTTYPES TestType, const PVOID TestData) +{ + const CHAR GetSuiteIDAction[] = "getsuiteid"; + const CHAR ModuleProp[] = "&module="; + const CHAR TestProp[] = "&test="; + + DWORD DataLength; + PCHAR Data; + PWINE_GETSUITEID_DATA WineData; + + DataLength = sizeof(ActionProp) - 1 + sizeof(GetSuiteIDAction) - 1; + DataLength += strlen(AuthenticationRequestString); + DataLength += sizeof(TestTypeProp) - 1; + + switch(TestType) + { + case WineTest: + DataLength += sizeof(WineTestType) - 1; + + WineData = (PWINE_GETSUITEID_DATA)TestData; + DataLength += sizeof(ModuleProp) - 1; + DataLength += strlen(WineData->Module); + DataLength += sizeof(TestProp) - 1; + DataLength += strlen(WineData->Test); + + break; + } + + Data = HeapAlloc(hProcessHeap, 0, DataLength + 1); + strcpy(Data, ActionProp); + strcat(Data, GetSuiteIDAction); + strcat(Data, AuthenticationRequestString); + strcat(Data, TestTypeProp); + + switch(TestType) + { + case WineTest: + strcat(Data, WineTestType); + + /* Stupid GCC and MSVC: WineData is already initialized above, still it's reported as a potentially uninitialized variable :-( */ + WineData = (PWINE_GETSUITEID_DATA)TestData; + strcat(Data, ModuleProp); + strcat(Data, WineData->Module); + strcat(Data, TestProp); + strcat(Data, WineData->Test); + + break; + } + + if(!IntDoRequest(&Data, &DataLength)) + return NULL; + + /* Verify that this is really a number */ + if(!IsNumber(Data)) + { + StringOut("Expected Suite ID, but received:\n"); + StringOut(Data); + HeapFree(hProcessHeap, 0, Data); + return NULL; + } + + return Data; +} + +/** + * Submits the result of one test call to the web service. + * + * @param TestType + * Value from the TESTTYPES enum indicating the type of test we are about to submit. + * + * @param TestData + * Pointer to a *_SUBMIT_DATA structure appropriate for our selected test type. + * Contains other input information for this request. + * + * @return + * TRUE if everything went well, FALSE otherwise. + */ +BOOL +Submit(TESTTYPES TestType, const PVOID TestData) +{ + const CHAR SubmitAction[] = "submit"; + const CHAR SuiteIDProp[] = "&suiteid="; + const CHAR LogProp[] = "&log="; + + DWORD DataLength; + PCHAR Data; + PCHAR pData; + PGENERAL_SUBMIT_DATA GeneralData; + PWINE_SUBMIT_DATA WineData; + + /* Compute the full length of the POST data */ + DataLength = sizeof(ActionProp) - 1 + sizeof(SubmitAction) - 1; + DataLength += strlen(AuthenticationRequestString); + + GeneralData = (PGENERAL_SUBMIT_DATA)TestData; + DataLength += sizeof(TestIDProp) - 1; + DataLength += strlen(GeneralData->TestID); + DataLength += sizeof(SuiteIDProp) - 1; + DataLength += strlen(GeneralData->SuiteID); + + /* The rest of the POST data depends on the test type */ + DataLength += sizeof(TestTypeProp) - 1; + + switch(TestType) + { + case WineTest: + DataLength += sizeof(WineTestType) - 1; + + WineData = (PWINE_SUBMIT_DATA)TestData; + DataLength += sizeof(LogProp) - 1; + DataLength += 3 * strlen(WineData->Log); + + break; + } + + /* Now collect all the POST data */ + Data = HeapAlloc(hProcessHeap, 0, DataLength + 1); + strcpy(Data, ActionProp); + strcat(Data, SubmitAction); + strcat(Data, AuthenticationRequestString); + + strcat(Data, TestIDProp); + strcat(Data, GeneralData->TestID); + strcat(Data, SuiteIDProp); + strcat(Data, GeneralData->SuiteID); + + strcat(Data, TestTypeProp); + + switch(TestType) + { + case WineTest: + strcat(Data, WineTestType); + + /* Stupid GCC and MSVC: WineData is already initialized above, still it's reported as a potentially uninitialized variable :-( */ + WineData = (PWINE_SUBMIT_DATA)TestData; + + strcat(Data, LogProp); + pData = Data + strlen(Data); + EscapeString(pData, WineData->Log); + + break; + } + + /* DataLength still contains the maximum length of the buffer, but not the actual data length we need for the request. + Determine that one now. */ + DataLength = strlen(Data); + + /* Send all the stuff */ + if(!IntDoRequest(&Data, &DataLength)) + return FALSE; + + /* Output the response */ + StringOut("The server responded:\n"); + StringOut(Data); + StringOut("\n"); + + if(!strcmp(Data, "OK")) + return TRUE; + + return FALSE; +} + +/** + * Finishes a test run for the web service. + * + * @param TestType + * Value from the TESTTYPES enum indicating the type of test we are about to submit. + * + * @param TestData + * Pointer to a *_FINISH_DATA structure appropriate for our selected test type. + * Contains other input information for this request. + * + * @return + * TRUE if everything went well, FALSE otherwise. + */ +BOOL +Finish(TESTTYPES TestType, const PVOID TestData) +{ + const CHAR FinishAction[] = "finish"; + + DWORD DataLength; + PCHAR Data; + PGENERAL_FINISH_DATA GeneralData; + + /* Build the full request string */ + DataLength = sizeof(ActionProp) - 1 + sizeof(FinishAction) - 1; + DataLength += strlen(AuthenticationRequestString); + + GeneralData = (PGENERAL_FINISH_DATA)TestData; + DataLength += sizeof(TestIDProp) - 1; + DataLength += strlen(GeneralData->TestID); + + DataLength += sizeof(TestTypeProp) - 1; + + switch(TestType) + { + case WineTest: + DataLength += sizeof(WineTestType) - 1; + break; + } + + Data = HeapAlloc(hProcessHeap, 0, DataLength + 1); + strcpy(Data, ActionProp); + strcat(Data, FinishAction); + strcat(Data, AuthenticationRequestString); + strcat(Data, TestIDProp); + strcat(Data, GeneralData->TestID); + strcat(Data, TestTypeProp); + + switch(TestType) + { + case WineTest: + strcat(Data, WineTestType); + break; + } + + if(!IntDoRequest(&Data, &DataLength)) + return FALSE; + + if(!strcmp(Data, "OK")) + return TRUE; + + return FALSE; +} diff --git a/rostests/rosautotest/winetests.c b/rostests/rosautotest/winetests.c new file mode 100644 index 00000000000..e9f2bbc2d1a --- /dev/null +++ b/rostests/rosautotest/winetests.c @@ -0,0 +1,374 @@ +/* + * PROJECT: ReactOS Automatic Testing Utility + * LICENSE: GNU GPLv2 or any later version as published by the Free Software Foundation + * PURPOSE: Running Wine Tests automatically + * COPYRIGHT: Copyright 2008-2009 Colin Finck + */ + +#include "precomp.h" + +/** + * Runs a specific test for a specific module. + * If we get results for a test, they are submitted to the Web Service. + * + * @param CommandLine + * Command line to run (should be the path to a module's test suite together with a parameter for the specified test) + * + * @param hReadPipe + * Handle to the Read Pipe set up in RunWineTests. + * + * @param StartupInfo + * Pointer to the StartupInfo structure set up in RunWineTests. + * + * @param GetSuiteIDData + * Pointer to the GetSuiteIDData structure set up in IntRunModuleTests. + * + * @param SubmitData + * Pointer to the SubmitData structure set up in RunWineTests. + * + * @return + * TRUE if everything went well, FALSE otherwise. + */ +static BOOL +IntRunTest(PWSTR CommandLine, HANDLE hReadPipe, LPSTARTUPINFOW StartupInfo, PWINE_GETSUITEID_DATA GetSuiteIDData, PWINE_SUBMIT_DATA SubmitData) +{ + BOOL BreakLoop = FALSE; + DWORD BytesAvailable; + DWORD LogAvailable = 0; + DWORD LogLength = 0; + DWORD LogPosition = 0; + DWORD Temp; + PCHAR Buffer; + PROCESS_INFORMATION ProcessInfo; + + if(AppOptions.Submit) + { + /* Allocate one block for the log */ + SubmitData->Log = HeapAlloc(hProcessHeap, 0, BUFFER_BLOCKSIZE); + LogAvailable = BUFFER_BLOCKSIZE; + LogLength = BUFFER_BLOCKSIZE; + } + + /* Execute the test */ + StringOut("Running Wine Test, Module: "); + StringOut(GetSuiteIDData->Module); + StringOut(", Test: "); + StringOut(GetSuiteIDData->Test); + StringOut("\n"); + + if(!CreateProcessW(NULL, CommandLine, NULL, NULL, TRUE, NORMAL_PRIORITY_CLASS, NULL, NULL, StartupInfo, &ProcessInfo)) + { + StringOut("CreateProcessW for running the test failed\n"); + return FALSE; + } + + /* Receive all the data from the pipe */ + do + { + /* When the application finished, make sure that we peek the pipe one more time, so that we get all data. + If the following condition would be the while() condition, we might hit a race condition: + - We check for data with PeekNamedPipe -> no data available + - The application outputs its data and finishes + - WaitForSingleObject reports that the application has finished and we break the loop without receiving any data + */ + if(WaitForSingleObject(ProcessInfo.hProcess, 0) == WAIT_OBJECT_0) + BreakLoop = TRUE; + + if(!PeekNamedPipe(hReadPipe, NULL, 0, NULL, &BytesAvailable, NULL)) + { + StringOut("PeekNamedPipe failed for the test run\n"); + return FALSE; + } + + if(BytesAvailable) + { + /* There is data, so get it and output it */ + Buffer = HeapAlloc(hProcessHeap, 0, BytesAvailable + 1); + + if(!ReadFile(hReadPipe, Buffer, BytesAvailable, &Temp, NULL)) + { + StringOut("ReadFile failed for the test run\n"); + return FALSE; + } + + /* Output all test output through StringOut, even while the test is still running */ + Buffer[BytesAvailable] = 0; + StringOut(Buffer); + + if(AppOptions.Submit) + { + /* Also store it in the buffer */ + if(BytesAvailable > LogAvailable) + { + /* Allocate enough new blocks to hold all available data */ + Temp = ((BytesAvailable - LogAvailable) / BUFFER_BLOCKSIZE + 1) * BUFFER_BLOCKSIZE; + LogAvailable += Temp; + LogLength += Temp; + SubmitData->Log = HeapReAlloc(hProcessHeap, 0, SubmitData->Log, LogLength); + } + + memcpy(&SubmitData->Log[LogPosition], Buffer, BytesAvailable); + LogPosition += BytesAvailable; + LogAvailable -= BytesAvailable; + } + + HeapFree(hProcessHeap, 0, Buffer); + } + } + while(!BreakLoop); + + if(AppOptions.Submit) + { + SubmitData->Log[LogLength - LogAvailable] = 0; + + /* If we got any output, submit it to the web service */ + if(*SubmitData->Log) + { + /* We don't want to waste any ID's, so only request them if we can be sure that we have results to submit. */ + + /* Get a Test ID if we don't have one yet */ + if(!SubmitData->General.TestID) + { + SubmitData->General.TestID = GetTestID(WineTest); + + if(!SubmitData->General.TestID) + return FALSE; + } + + /* Get a Suite ID for this combination */ + SubmitData->General.SuiteID = GetSuiteID(WineTest, GetSuiteIDData); + + if(!SubmitData->General.SuiteID) + return FALSE; + + /* Submit the stuff */ + Submit(WineTest, SubmitData); + + /* Cleanup */ + HeapFree(hProcessHeap, 0, SubmitData->General.SuiteID); + } + + /* Cleanup */ + HeapFree(hProcessHeap, 0, SubmitData->Log); + } + + StringOut("\n\n"); + + return TRUE; +} + +/** + * Runs the desired tests for a specified module. + * + * @param File + * The file name of the module's test suite. + * + * @param FilePath + * The full path to the file of the module's test suite. + * + * @param hReadPipe + * Handle to the Read Pipe set up in RunWineTests. + * + * @param StartupInfo + * Pointer to the StartupInfo structure set up in RunWineTests. + * + * @param SubmitData + * Pointer to the SubmitData structure set up in RunWineTests. + * + * @return + * TRUE if everything went well, FALSE otherwise. + */ +static BOOL +IntRunModuleTests(PWSTR File, PWSTR FilePath, HANDLE hReadPipe, LPSTARTUPINFOW StartupInfo, PWINE_SUBMIT_DATA SubmitData) +{ + DWORD BytesAvailable; + DWORD Length; + DWORD Temp; + PCHAR Buffer; + PCHAR pStart; + PCHAR pEnd; + PROCESS_INFORMATION ProcessInfo; + size_t FilePosition; + WINE_GETSUITEID_DATA GetSuiteIDData; + + /* Build the full command line */ + FilePosition = wcslen(FilePath); + FilePath[FilePosition++] = ' '; + FilePath[FilePosition] = 0; + wcscat(FilePath, L"--list"); + + /* Store the tested module name */ + Length = wcschr(File, L'_') - File; + GetSuiteIDData.Module = HeapAlloc(hProcessHeap, 0, Length + 1); + WideCharToMultiByte(CP_ACP, 0, File, Length, GetSuiteIDData.Module, Length, NULL, NULL); + GetSuiteIDData.Module[Length] = 0; + + /* Start the process for getting all available tests */ + if(!CreateProcessW(NULL, FilePath, NULL, NULL, TRUE, NORMAL_PRIORITY_CLASS, NULL, NULL, StartupInfo, &ProcessInfo)) + { + StringOut("CreateProcessW for getting the available tests failed\n"); + return FALSE; + } + + /* Wait till this process ended */ + if(WaitForSingleObject(ProcessInfo.hProcess, INFINITE) == WAIT_FAILED) + { + StringOut("WaitForSingleObject failed for the test list\n"); + return FALSE; + } + + /* Read the output data into a buffer */ + if(!PeekNamedPipe(hReadPipe, NULL, 0, NULL, &BytesAvailable, NULL)) + { + StringOut("PeekNamedPipe failed for the test list\n"); + return FALSE; + } + + Buffer = HeapAlloc(hProcessHeap, 0, BytesAvailable); + + if(!ReadFile(hReadPipe, Buffer, BytesAvailable, &Temp, NULL)) + { + StringOut("ReadFile failed\n"); + return FALSE; + } + + /* Jump to the first available test */ + pStart = strchr(Buffer, '\n'); + pStart += 5; + + while(pStart < (Buffer + BytesAvailable)) + { + /* Get start and end of this test name */ + pEnd = pStart; + + while(*pEnd != '\r') + ++pEnd; + + /* Store the test name */ + GetSuiteIDData.Test = HeapAlloc(hProcessHeap, 0, pEnd - pStart + 1); + memcpy(GetSuiteIDData.Test, pStart, pEnd - pStart); + GetSuiteIDData.Test[pEnd - pStart] = 0; + + /* If the user gave us a test to run, we check whether the module's test suite really provides this test. */ + if(!AppOptions.Test || (AppOptions.Test && !strcmp(AppOptions.Test, GetSuiteIDData.Test))) + { + /* Build the command line for this test */ + Length = MultiByteToWideChar(CP_ACP, 0, pStart, pEnd - pStart, NULL, 0); + MultiByteToWideChar(CP_ACP, 0, pStart, pEnd - pStart, &FilePath[FilePosition], Length * sizeof(WCHAR)); + FilePath[FilePosition + Length] = 0; + + if(!IntRunTest(FilePath, hReadPipe, StartupInfo, &GetSuiteIDData, SubmitData)) + return FALSE; + } + + /* Cleanup */ + HeapFree(hProcessHeap, 0, GetSuiteIDData.Test); + + /* Move to the next test */ + pStart = pEnd + 6; + } + + /* Cleanup */ + HeapFree(hProcessHeap, 0, GetSuiteIDData.Module); + HeapFree(hProcessHeap, 0, Buffer); + + return TRUE; +} + +/** + * Runs the Wine tests according to the options specified by the parameters. + * + * @return + * TRUE if everything went well, FALSE otherwise. + */ +BOOL +RunWineTests() +{ + GENERAL_FINISH_DATA FinishData; + HANDLE hFind; + HANDLE hReadPipe; + HANDLE hWritePipe; + SECURITY_ATTRIBUTES SecurityAttributes; + STARTUPINFOW StartupInfo = {0}; + size_t PathPosition; + WCHAR FilePath[MAX_PATH]; + WIN32_FIND_DATAW fd; + WINE_SUBMIT_DATA SubmitData = { {0} }; + + /* Create a pipe for getting the output of the tests */ + SecurityAttributes.nLength = sizeof(SecurityAttributes); + SecurityAttributes.bInheritHandle = TRUE; + SecurityAttributes.lpSecurityDescriptor = NULL; + + if(!CreatePipe(&hReadPipe, &hWritePipe, &SecurityAttributes, 0)) + { + StringOut("CreatePipe failed\n"); + return FALSE; + } + + StartupInfo.cb = sizeof(StartupInfo); + StartupInfo.dwFlags = STARTF_USESTDHANDLES; + StartupInfo.hStdOutput = hWritePipe; + + /* Build the path for finding the tests */ + if(GetWindowsDirectoryW(FilePath, MAX_PATH) > MAX_PATH - 60) + { + StringOut("Windows directory path is too long\n"); + return FALSE; + } + + wcscat(FilePath, L"\\bin\\"); + PathPosition = wcslen(FilePath); + + if(AppOptions.Module) + { + /* Find a test belonging to this module */ + wcscat(FilePath, AppOptions.Module); + wcscat(FilePath, L"_*.exe"); + } + else + { + /* Find all tests */ + wcscat(FilePath, L"*.exe"); + } + + hFind = FindFirstFileW(FilePath, &fd); + + if(hFind == INVALID_HANDLE_VALUE) + { + StringOut("FindFirstFileW failed\n"); + return FALSE; + } + + /* Run the tests */ + do + { + /* Build the full path to the test suite */ + wcscpy(&FilePath[PathPosition], fd.cFileName); + + /* Run it */ + if(!IntRunModuleTests(fd.cFileName, FilePath, hReadPipe, &StartupInfo, &SubmitData)) + return FALSE; + } + while(FindNextFileW(hFind, &fd)); + + /* Cleanup */ + FindClose(hFind); + + if(AppOptions.Submit && SubmitData.General.TestID) + { + /* We're done with the tests, so close this test run */ + FinishData.TestID = SubmitData.General.TestID; + + if(!Finish(WineTest, &FinishData)) + return FALSE; + + /* Cleanup */ + HeapFree(hProcessHeap, 0, FinishData.TestID); + } + + CloseHandle(hReadPipe); + CloseHandle(hWritePipe); + + return TRUE; +}