reactos/modules/rostests/rosautotest/CWineTest.cpp

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);
}