Introducing the "ReactOS Automatic Testing Utility", superseding our current syssetup...
authorColin Finck <colin@reactos.org>
Mon, 5 Jan 2009 12:41:34 +0000 (12:41 +0000)
committerColin Finck <colin@reactos.org>
Mon, 5 Jan 2009 12:41:34 +0000 (12:41 +0000)
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

rostests/directory.rbuild
rostests/rosautotest/main.c [new file with mode: 0644]
rostests/rosautotest/precomp.h [new file with mode: 0644]
rostests/rosautotest/rosautotest.rbuild [new file with mode: 0644]
rostests/rosautotest/shutdown.c [new file with mode: 0644]
rostests/rosautotest/tools.c [new file with mode: 0644]
rostests/rosautotest/webservice.c [new file with mode: 0644]
rostests/rosautotest/winetests.c [new file with mode: 0644]

index bbd656e..5dafb2d 100644 (file)
@@ -13,6 +13,9 @@
        <directory name="regtests_by_casper">
                <xi:include href="regtests_by_casper/directory.rbuild" />
        </directory>
+       <directory name="rosautotest">
+               <xi:include href="rosautotest/rosautotest.rbuild" />
+       </directory>
        <directory name="tests">
                <xi:include href="tests/directory.rbuild" />
        </directory>
diff --git a/rostests/rosautotest/main.c b/rostests/rosautotest/main.c
new file mode 100644 (file)
index 0000000..31c984c
--- /dev/null
@@ -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 <colin@reactos.org>
+ */
+
+#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 (file)
index 0000000..87fdf2d
--- /dev/null
@@ -0,0 +1,81 @@
+/* Includes */
+#include <stdio.h>
+
+#include <windows.h>
+#include <reason.h>
+#include <wininet.h>
+
+#include <reactos/buildno.h>
+
+/* 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 (file)
index 0000000..b372227
--- /dev/null
@@ -0,0 +1,15 @@
+<?xml version="1.0"?>
+<!DOCTYPE module SYSTEM "../../../tools/rbuild/project.dtd">
+<module name="rosautotest" type="win32cui" installbase="system32" installname="rosautotest.exe" unicode="yes">
+       <include base="rosautotest">.</include>
+       <library>advapi32</library>
+       <library>kernel32</library>
+       <library>user32</library>
+       <library>wininet</library>
+       <file>main.c</file>
+       <file>shutdown.c</file>
+       <file>tools.c</file>
+       <file>webservice.c</file>
+       <file>winetests.c</file>
+       <pch>precomp.h</pch>
+</module>
diff --git a/rostests/rosautotest/shutdown.c b/rostests/rosautotest/shutdown.c
new file mode 100644 (file)
index 0000000..7493d79
--- /dev/null
@@ -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 <colin@reactos.org>
+ */
+
+#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 (file)
index 0000000..763c817
--- /dev/null
@@ -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 <colin@reactos.org>
+ */
+
+#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 (file)
index 0000000..dcfd79f
--- /dev/null
@@ -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 <colin@reactos.org>
+ */
+
+#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 (file)
index 0000000..e9f2bbc
--- /dev/null
@@ -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 <colin@reactos.org>
+ */
+
+#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;
+}