mirror of
https://github.com/reactos/reactos.git
synced 2024-12-29 02:25:17 +00:00
416 lines
12 KiB
C++
416 lines
12 KiB
C++
/*
|
|
* PROJECT: ReactOS Automatic Testing Utility
|
|
* LICENSE: GPL-2.0+ (https://spdx.org/licenses/GPL-2.0+)
|
|
* PURPOSE: Class implementing functions for handling Wine tests
|
|
* COPYRIGHT: Copyright 2009-2019 Colin Finck (colin@reactos.org)
|
|
*/
|
|
|
|
#include "precomp.h"
|
|
|
|
static const DWORD ListTimeout = 10000;
|
|
|
|
// This value needs to be lower than the <timeout> configured in sysreg.xml! (usually 180000)
|
|
// Otherwise, sysreg2 kills the VM before we can kill the process.
|
|
static const DWORD ProcessActivityTimeout = 170000;
|
|
|
|
|
|
/**
|
|
* Constructs a CWineTest object.
|
|
*/
|
|
CWineTest::CWineTest()
|
|
: m_hFind(NULL), m_ListBuffer(NULL)
|
|
{
|
|
WCHAR wszDirectory[MAX_PATH];
|
|
|
|
/* Set up m_TestPath */
|
|
if (GetEnvironmentVariableW(L"ROSAUTOTEST_DIR", wszDirectory, MAX_PATH))
|
|
{
|
|
m_TestPath = wszDirectory;
|
|
if (*m_TestPath.rbegin() != L'\\')
|
|
m_TestPath += L'\\';
|
|
}
|
|
else
|
|
{
|
|
if (!GetWindowsDirectoryW(wszDirectory, MAX_PATH))
|
|
FATAL("GetWindowsDirectoryW failed\n");
|
|
|
|
m_TestPath = wszDirectory;
|
|
m_TestPath += L"\\bin\\";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Destructs a CWineTest object.
|
|
*/
|
|
CWineTest::~CWineTest()
|
|
{
|
|
if(m_hFind)
|
|
FindClose(m_hFind);
|
|
|
|
if(m_ListBuffer)
|
|
delete m_ListBuffer;
|
|
}
|
|
|
|
/**
|
|
* Gets the next module test file using the FindFirstFileW/FindNextFileW API.
|
|
*
|
|
* @return
|
|
* true if we found a next file, otherwise false.
|
|
*/
|
|
bool
|
|
CWineTest::GetNextFile()
|
|
{
|
|
bool FoundFile = false;
|
|
WIN32_FIND_DATAW fd;
|
|
|
|
/* Did we already begin searching for files? */
|
|
if(m_hFind)
|
|
{
|
|
/* Then get the next file (if any) */
|
|
if(FindNextFileW(m_hFind, &fd))
|
|
FoundFile = true;
|
|
}
|
|
else
|
|
{
|
|
/* Start searching for test files */
|
|
wstring FindPath = m_TestPath;
|
|
|
|
/* Did the user specify a module? */
|
|
if(Configuration.GetModule().empty())
|
|
{
|
|
/* No module, so search for all files in that directory */
|
|
FindPath += L"*.exe";
|
|
}
|
|
else
|
|
{
|
|
/* Search for files with the pattern "modulename_*" */
|
|
FindPath += Configuration.GetModule();
|
|
FindPath += L"_*.exe";
|
|
}
|
|
|
|
/* Search for the first file and check whether we got one */
|
|
m_hFind = FindFirstFileW(FindPath.c_str(), &fd);
|
|
|
|
if(m_hFind != INVALID_HANDLE_VALUE)
|
|
FoundFile = true;
|
|
}
|
|
|
|
if(FoundFile)
|
|
m_CurrentFile = fd.cFileName;
|
|
|
|
return FoundFile;
|
|
}
|
|
|
|
/**
|
|
* Executes the --list command of a module test file to get information about the available tests.
|
|
*
|
|
* @return
|
|
* The number of bytes we read into the m_ListBuffer member variable by capturing the output of the --list command.
|
|
*/
|
|
DWORD
|
|
CWineTest::DoListCommand()
|
|
{
|
|
DWORD BytesAvailable;
|
|
DWORD Temp;
|
|
wstring CommandLine;
|
|
CPipe Pipe;
|
|
|
|
/* Build the command line */
|
|
CommandLine = m_TestPath;
|
|
CommandLine += m_CurrentFile;
|
|
CommandLine += L" --list";
|
|
|
|
{
|
|
/* Start the process for getting all available tests */
|
|
CPipedProcess Process(CommandLine, Pipe);
|
|
|
|
/* Wait till this process ended */
|
|
if(WaitForSingleObject(Process.GetProcessHandle(), ListTimeout) == WAIT_FAILED)
|
|
TESTEXCEPTION("WaitForSingleObject failed for the test list\n");
|
|
}
|
|
|
|
/* Read the output data into a buffer */
|
|
if(!Pipe.Peek(NULL, 0, NULL, &BytesAvailable))
|
|
TESTEXCEPTION("CPipe::Peek failed for the test list\n");
|
|
|
|
/* Check if we got any */
|
|
if(!BytesAvailable)
|
|
{
|
|
stringstream ss;
|
|
|
|
ss << "The --list command did not return any data for " << UnicodeToAscii(m_CurrentFile) << endl;
|
|
TESTEXCEPTION(ss.str());
|
|
}
|
|
|
|
/* Read the data */
|
|
m_ListBuffer = new char[BytesAvailable];
|
|
|
|
if(Pipe.Read(m_ListBuffer, BytesAvailable, &Temp, INFINITE) != ERROR_SUCCESS)
|
|
TESTEXCEPTION("CPipe::Read failed\n");
|
|
|
|
return BytesAvailable;
|
|
}
|
|
|
|
/**
|
|
* Gets the next test from m_ListBuffer, which was filled with information from the --list command.
|
|
*
|
|
* @return
|
|
* true if a next test was found, otherwise false.
|
|
*/
|
|
bool
|
|
CWineTest::GetNextTest()
|
|
{
|
|
PCHAR pEnd;
|
|
static DWORD BufferSize;
|
|
static PCHAR pStart;
|
|
|
|
if(!m_ListBuffer)
|
|
{
|
|
/* Perform the --list command */
|
|
BufferSize = DoListCommand();
|
|
|
|
/* Move the pointer to the first test */
|
|
pStart = strchr(m_ListBuffer, '\n');
|
|
pStart += 5;
|
|
}
|
|
|
|
/* If we reach the buffer size, we finished analyzing the output of this test */
|
|
if(pStart >= (m_ListBuffer + BufferSize))
|
|
{
|
|
/* Clear m_CurrentFile to indicate that */
|
|
m_CurrentFile.clear();
|
|
|
|
/* Also free the memory for the list buffer */
|
|
delete[] m_ListBuffer;
|
|
m_ListBuffer = NULL;
|
|
|
|
return false;
|
|
}
|
|
|
|
/* Get start and end of this test name */
|
|
pEnd = pStart;
|
|
|
|
while(*pEnd != '\r')
|
|
++pEnd;
|
|
|
|
/* Store the test name */
|
|
m_CurrentTest = string(pStart, pEnd);
|
|
|
|
/* Move the pointer to the next test */
|
|
pStart = pEnd + 6;
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Interface to CTestList-derived classes for getting all information about the next test to be run.
|
|
*
|
|
* @return
|
|
* Returns a pointer to a CTestInfo object containing all available information about the next test.
|
|
*/
|
|
CTestInfo*
|
|
CWineTest::GetNextTestInfo()
|
|
{
|
|
while(!m_CurrentFile.empty() || GetNextFile())
|
|
{
|
|
/* The user asked for a list of all modules */
|
|
if (Configuration.ListModulesOnly())
|
|
{
|
|
std::stringstream ss;
|
|
ss << "Module: " << UnicodeToAscii(m_CurrentFile) << endl;
|
|
m_CurrentFile.clear();
|
|
StringOut(ss.str());
|
|
continue;
|
|
}
|
|
|
|
try
|
|
{
|
|
while(GetNextTest())
|
|
{
|
|
/* If the user specified a test through the command line, check this here */
|
|
if(!Configuration.GetTest().empty() && Configuration.GetTest() != m_CurrentTest)
|
|
continue;
|
|
|
|
{
|
|
auto_ptr<CTestInfo> TestInfo(new CTestInfo());
|
|
size_t UnderscorePosition;
|
|
|
|
/* Build the command line */
|
|
TestInfo->CommandLine = m_TestPath;
|
|
TestInfo->CommandLine += m_CurrentFile;
|
|
TestInfo->CommandLine += ' ';
|
|
TestInfo->CommandLine += AsciiToUnicode(m_CurrentTest);
|
|
|
|
/* Store the Module name */
|
|
UnderscorePosition = m_CurrentFile.find_last_of('_');
|
|
|
|
if(UnderscorePosition == m_CurrentFile.npos)
|
|
{
|
|
stringstream ss;
|
|
|
|
ss << "Invalid test file name: " << UnicodeToAscii(m_CurrentFile) << endl;
|
|
SSEXCEPTION;
|
|
}
|
|
|
|
TestInfo->Module = UnicodeToAscii(m_CurrentFile.substr(0, UnderscorePosition));
|
|
|
|
/* Store the test */
|
|
TestInfo->Test = m_CurrentTest;
|
|
|
|
return TestInfo.release();
|
|
}
|
|
}
|
|
}
|
|
catch(CTestException& e)
|
|
{
|
|
stringstream ss;
|
|
|
|
ss << "An exception occurred trying to list tests for: " << UnicodeToAscii(m_CurrentFile) << endl;
|
|
StringOut(ss.str());
|
|
StringOut(e.GetMessage());
|
|
StringOut("\n");
|
|
m_CurrentFile.clear();
|
|
delete[] m_ListBuffer;
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/**
|
|
* Runs a Wine test and captures the output
|
|
*
|
|
* @param TestInfo
|
|
* Pointer to a CTestInfo object containing information about the test.
|
|
* Will contain the test log afterwards if the user wants to submit data.
|
|
*/
|
|
void
|
|
CWineTest::RunTest(CTestInfo* TestInfo)
|
|
{
|
|
DWORD BytesAvailable;
|
|
stringstream ss, ssFinish;
|
|
DWORD StartTime;
|
|
float TotalTime;
|
|
string tailString;
|
|
CPipe Pipe;
|
|
char Buffer[1024];
|
|
|
|
ss << "Running Wine Test, Module: " << TestInfo->Module << ", Test: " << TestInfo->Test << endl;
|
|
StringOut(ss.str());
|
|
|
|
SetCurrentDirectoryW(m_TestPath.c_str());
|
|
|
|
StartTime = GetTickCount();
|
|
|
|
try
|
|
{
|
|
/* Execute the test */
|
|
CPipedProcess Process(TestInfo->CommandLine, Pipe);
|
|
|
|
/* Receive all the data from the pipe */
|
|
for (;;)
|
|
{
|
|
DWORD dwReadResult = Pipe.Read(Buffer, sizeof(Buffer) - 1, &BytesAvailable, ProcessActivityTimeout);
|
|
if (dwReadResult == ERROR_SUCCESS)
|
|
{
|
|
/* Output text through StringOut, even while the test is still running */
|
|
Buffer[BytesAvailable] = 0;
|
|
tailString = StringOut(tailString.append(string(Buffer)), false);
|
|
|
|
if (Configuration.DoSubmit())
|
|
TestInfo->Log += Buffer;
|
|
}
|
|
else if (dwReadResult == ERROR_BROKEN_PIPE)
|
|
{
|
|
// The process finished and has been terminated.
|
|
break;
|
|
}
|
|
else if (dwReadResult == WAIT_TIMEOUT)
|
|
{
|
|
// The process activity timeout above has elapsed without any new data.
|
|
TESTEXCEPTION("Timeout while waiting for the test process\n");
|
|
}
|
|
else
|
|
{
|
|
// An unexpected error.
|
|
TESTEXCEPTION("CPipe::Read failed for the test run\n");
|
|
}
|
|
}
|
|
}
|
|
catch(CTestException& e)
|
|
{
|
|
if(!tailString.empty())
|
|
StringOut(tailString);
|
|
tailString.clear();
|
|
StringOut(e.GetMessage());
|
|
TestInfo->Log += e.GetMessage();
|
|
}
|
|
|
|
/* Print what's left */
|
|
if(!tailString.empty())
|
|
StringOut(tailString);
|
|
|
|
TotalTime = ((float)GetTickCount() - StartTime)/1000;
|
|
ssFinish << "Test " << TestInfo->Test << " completed in ";
|
|
ssFinish << setprecision(2) << fixed << TotalTime << " seconds." << endl;
|
|
StringOut(ssFinish.str());
|
|
TestInfo->Log += ssFinish.str();
|
|
}
|
|
|
|
/**
|
|
* Interface to other classes for running all desired Wine tests.
|
|
*/
|
|
void
|
|
CWineTest::Run()
|
|
{
|
|
auto_ptr<CTestList> TestList;
|
|
auto_ptr<CWebService> WebService;
|
|
CTestInfo* TestInfo;
|
|
DWORD ErrorMode;
|
|
|
|
/* The virtual test list is of course faster, so it should be preferred over
|
|
the journaled one.
|
|
Enable the journaled one only in case ...
|
|
- we're running under ReactOS (as the journal is only useful in conjunction with sysreg2)
|
|
- we shall keep information for Crash Recovery
|
|
- and the user didn't specify a module (then doing Crash Recovery doesn't really make sense) */
|
|
if(Configuration.IsReactOS() && Configuration.DoCrashRecovery() && Configuration.GetModule().empty())
|
|
{
|
|
/* Use a test list with a permanent journal */
|
|
TestList.reset(new CJournaledTestList(this));
|
|
}
|
|
else
|
|
{
|
|
/* Use the fast virtual test list with no additional overhead */
|
|
TestList.reset(new CVirtualTestList(this));
|
|
}
|
|
|
|
/* Initialize the Web Service interface if required */
|
|
if(Configuration.DoSubmit())
|
|
WebService.reset(new CWebService());
|
|
|
|
/* Disable error dialogs if we're running in non-interactive mode */
|
|
if(!Configuration.IsInteractive())
|
|
ErrorMode = SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX);
|
|
|
|
/* Get information for each test to run */
|
|
while((TestInfo = TestList->GetNextTestInfo()) != 0)
|
|
{
|
|
auto_ptr<CTestInfo> TestInfoPtr(TestInfo);
|
|
|
|
RunTest(TestInfo);
|
|
|
|
if(Configuration.DoSubmit() && !TestInfo->Log.empty())
|
|
WebService->Submit("wine", TestInfo);
|
|
|
|
StringOut("\n\n");
|
|
}
|
|
|
|
/* We're done with all tests. Finish this run */
|
|
if(Configuration.DoSubmit())
|
|
WebService->Finish("wine");
|
|
|
|
/* Restore the original error mode */
|
|
if(!Configuration.IsInteractive())
|
|
SetErrorMode(ErrorMode);
|
|
}
|