Reactos
at master 460 lines 13 kB view raw
1/* 2 * PROJECT: ReactOS Automatic Testing Utility 3 * LICENSE: GPL-2.0+ (https://spdx.org/licenses/GPL-2.0+) 4 * PURPOSE: Class implementing functions for handling Wine tests 5 * COPYRIGHT: Copyright 2009-2019 Colin Finck (colin@reactos.org) 6 */ 7 8#include "precomp.h" 9 10static const DWORD ListTimeout = 10000; 11 12// This value needs to be lower than the <timeout> configured in sysreg.xml! (usually 180000) 13// Otherwise, sysreg2 kills the VM before we can kill the process. 14static const DWORD ProcessActivityTimeout = 170000; 15 16 17/** 18 * Constructs a CWineTest object. 19 */ 20CWineTest::CWineTest() 21 : m_hFind(NULL), m_ListBuffer(NULL) 22{ 23 WCHAR wszDirectory[MAX_PATH]; 24 25 /* Set up m_TestPath */ 26 if (GetEnvironmentVariableW(L"ROSAUTOTEST_DIR", wszDirectory, MAX_PATH)) 27 { 28 m_TestPath = wszDirectory; 29 if (*m_TestPath.rbegin() != L'\\') 30 m_TestPath += L'\\'; 31 } 32 else 33 { 34 if (!GetWindowsDirectoryW(wszDirectory, MAX_PATH)) 35 FATAL("GetWindowsDirectoryW failed\n"); 36 37 m_TestPath = wszDirectory; 38 m_TestPath += L"\\bin\\"; 39 } 40} 41 42/** 43 * Destructs a CWineTest object. 44 */ 45CWineTest::~CWineTest() 46{ 47 if(m_hFind) 48 FindClose(m_hFind); 49 50 if(m_ListBuffer) 51 delete m_ListBuffer; 52} 53 54/** 55 * Gets the next module test file using the FindFirstFileW/FindNextFileW API. 56 * 57 * @return 58 * true if we found a next file, otherwise false. 59 */ 60bool 61CWineTest::GetNextFile() 62{ 63 bool FoundFile = false; 64 WIN32_FIND_DATAW fd; 65 66 /* Did we already begin searching for files? */ 67 if (m_hFind) 68 { 69 /* Then get the next file (if any) */ 70 if (FindNextFileW(m_hFind, &fd)) 71 { 72 // printf("cFileName is '%S'.\n", fd.cFileName); 73 /* If it was NOT rosautotest.exe then proceed as normal */ 74 if (_wcsicmp(fd.cFileName, TestName) != 0) 75 { 76 FoundFile = true; 77 } 78 else 79 { 80 /* It was rosautotest.exe so get the next file (if any) */ 81 if (FindNextFileW(m_hFind, &fd)) 82 { 83 FoundFile = true; 84 } 85 // printf("cFileName is '%S'.\n", fd.cFileName); 86 } 87 } 88 } 89 else 90 { 91 /* Start searching for test files */ 92 wstring FindPath = m_TestPath; 93 94 /* Did the user specify a module? */ 95 if(Configuration.GetModule().empty()) 96 { 97 /* No module, so search for all files in that directory */ 98 FindPath += L"*.exe"; 99 } 100 else 101 { 102 /* Search for files with the pattern "modulename_*" */ 103 FindPath += Configuration.GetModule(); 104 FindPath += L"_*.exe"; 105 } 106 107 /* Search for the first file and check whether we got one */ 108 m_hFind = FindFirstFileW(FindPath.c_str(), &fd); 109 110 /* If we returned a good handle */ 111 if (m_hFind != INVALID_HANDLE_VALUE) 112 { 113 // printf("cFileName is '%S'.\n", fd.cFileName); 114 /* If it was NOT rosautotest.exe then proceed as normal */ 115 if (_wcsicmp(fd.cFileName, TestName) != 0) 116 { 117 FoundFile = true; 118 } 119 else 120 { 121 /* It was rosautotest.exe so get the next file (if any) */ 122 if (FindNextFileW(m_hFind, &fd)) 123 { 124 FoundFile = true; 125 } 126 // printf("cFileName is '%S'.\n", fd.cFileName); 127 } 128 } 129 } 130 131 if(FoundFile) 132 m_CurrentFile = fd.cFileName; 133 134 return FoundFile; 135} 136 137/** 138 * Executes the --list command of a module test file to get information about the available tests. 139 * 140 * @return 141 * The number of bytes we read into the m_ListBuffer member variable by capturing the output of the --list command. 142 */ 143DWORD 144CWineTest::DoListCommand() 145{ 146 DWORD BytesAvailable; 147 DWORD Temp; 148 wstring CommandLine; 149 CPipe Pipe; 150 151 /* Build the command line */ 152 CommandLine = m_TestPath; 153 CommandLine += m_CurrentFile; 154 CommandLine += L" --list"; 155 156 { 157 /* Start the process for getting all available tests */ 158 CPipedProcess Process(CommandLine, Pipe); 159 160 /* Wait till this process ended */ 161 if(WaitForSingleObject(Process.GetProcessHandle(), ListTimeout) == WAIT_FAILED) 162 TESTEXCEPTION("WaitForSingleObject failed for the test list\n"); 163 } 164 165 /* Read the output data into a buffer */ 166 if(!Pipe.Peek(NULL, 0, NULL, &BytesAvailable)) 167 TESTEXCEPTION("CPipe::Peek failed for the test list\n"); 168 169 /* Check if we got any */ 170 if(!BytesAvailable) 171 { 172 stringstream ss; 173 174 ss << "The --list command did not return any data for " << UnicodeToAscii(m_CurrentFile) << endl; 175 TESTEXCEPTION(ss.str()); 176 } 177 178 /* Read the data */ 179 m_ListBuffer = new char[BytesAvailable]; 180 181 if(Pipe.Read(m_ListBuffer, BytesAvailable, &Temp, INFINITE) != ERROR_SUCCESS) 182 TESTEXCEPTION("CPipe::Read failed\n"); 183 184 return BytesAvailable; 185} 186 187/** 188 * Gets the next test from m_ListBuffer, which was filled with information from the --list command. 189 * 190 * @return 191 * true if a next test was found, otherwise false. 192 */ 193bool 194CWineTest::GetNextTest() 195{ 196 PCHAR pEnd; 197 static DWORD BufferSize; 198 static PCHAR pStart; 199 200 if(!m_ListBuffer) 201 { 202 /* Perform the --list command */ 203 BufferSize = DoListCommand(); 204 205 /* Move the pointer to the first test */ 206 pStart = strchr(m_ListBuffer, '\n'); 207 pStart += 5; 208 } 209 210 /* If we reach the buffer size, we finished analyzing the output of this test */ 211 if(pStart >= (m_ListBuffer + BufferSize)) 212 { 213 /* Clear m_CurrentFile to indicate that */ 214 m_CurrentFile.clear(); 215 216 /* Also free the memory for the list buffer */ 217 delete[] m_ListBuffer; 218 m_ListBuffer = NULL; 219 220 return false; 221 } 222 223 /* Get start and end of this test name */ 224 pEnd = pStart; 225 226 while(*pEnd != '\r') 227 ++pEnd; 228 229 /* Store the test name */ 230 m_CurrentTest = string(pStart, pEnd); 231 232 /* Move the pointer to the next test */ 233 pStart = pEnd + 6; 234 235 return true; 236} 237 238/** 239 * Interface to CTestList-derived classes for getting all information about the next test to be run. 240 * 241 * @return 242 * Returns a pointer to a CTestInfo object containing all available information about the next test. 243 */ 244CTestInfo* 245CWineTest::GetNextTestInfo() 246{ 247 while(!m_CurrentFile.empty() || GetNextFile()) 248 { 249 /* The user asked for a list of all modules */ 250 if (Configuration.ListModulesOnly()) 251 { 252 std::stringstream ss; 253 ss << "Module: " << UnicodeToAscii(m_CurrentFile) << endl; 254 m_CurrentFile.clear(); 255 StringOut(ss.str()); 256 continue; 257 } 258 259 try 260 { 261 while(GetNextTest()) 262 { 263 /* If the user specified a test through the command line, check this here */ 264 if(!Configuration.GetTest().empty() && Configuration.GetTest() != m_CurrentTest) 265 continue; 266 267 { 268 auto_ptr<CTestInfo> TestInfo(new CTestInfo()); 269 size_t UnderscorePosition; 270 271 /* Build the command line */ 272 TestInfo->CommandLine = m_TestPath; 273 TestInfo->CommandLine += m_CurrentFile; 274 TestInfo->CommandLine += ' '; 275 TestInfo->CommandLine += AsciiToUnicode(m_CurrentTest); 276 277 /* Store the Module name */ 278 UnderscorePosition = m_CurrentFile.find_last_of('_'); 279 280 if(UnderscorePosition == m_CurrentFile.npos) 281 { 282 stringstream ss; 283 284 ss << "Invalid test file name: " << UnicodeToAscii(m_CurrentFile) << endl; 285 SSEXCEPTION; 286 } 287 288 TestInfo->Module = UnicodeToAscii(m_CurrentFile.substr(0, UnderscorePosition)); 289 290 /* Store the test */ 291 TestInfo->Test = m_CurrentTest; 292 293 return TestInfo.release(); 294 } 295 } 296 } 297 catch(CTestException& e) 298 { 299 stringstream ss; 300 301 ss << "An exception occurred trying to list tests for: " << UnicodeToAscii(m_CurrentFile) << endl; 302 StringOut(ss.str()); 303 StringOut(e.GetMessage()); 304 StringOut("\n"); 305 m_CurrentFile.clear(); 306 delete[] m_ListBuffer; 307 } 308 } 309 310 return NULL; 311} 312 313/** 314 * Runs a Wine test and captures the output 315 * 316 * @param TestInfo 317 * Pointer to a CTestInfo object containing information about the test. 318 * Will contain the test log afterwards if the user wants to submit data. 319 */ 320void 321CWineTest::RunTest(CTestInfo* TestInfo) 322{ 323 DWORD BytesAvailable; 324 stringstream ss, ssFinish; 325 DWORD StartTime; 326 float TotalTime; 327 string tailString; 328 CPipe Pipe; 329 char Buffer[1024]; 330 331 ss << "Running Wine Test, Module: " << TestInfo->Module << ", Test: " << TestInfo->Test << endl; 332 StringOut(ss.str()); 333 334 SetCurrentDirectoryW(m_TestPath.c_str()); 335 336 StartTime = GetTickCount(); 337 338 try 339 { 340 /* Execute the test */ 341 CPipedProcess Process(TestInfo->CommandLine, Pipe); 342 343 /* Receive all the data from the pipe */ 344 for (;;) 345 { 346 DWORD dwReadResult = Pipe.Read(Buffer, sizeof(Buffer) - 1, &BytesAvailable, ProcessActivityTimeout); 347 if (dwReadResult == ERROR_SUCCESS) 348 { 349 /* Output text through StringOut, even while the test is still running */ 350 Buffer[BytesAvailable] = 0; 351 tailString = StringOut(tailString.append(string(Buffer)), false); 352 353 if (Configuration.DoSubmit()) 354 TestInfo->Log += Buffer; 355 } 356 else if (dwReadResult == ERROR_BROKEN_PIPE) 357 { 358 // The process finished and has been terminated. 359 break; 360 } 361 else if (dwReadResult == WAIT_TIMEOUT) 362 { 363 // The process activity timeout above has elapsed without any new data. 364 TESTEXCEPTION("Timeout while waiting for the test process\n"); 365 } 366 else 367 { 368 // An unexpected error. 369 TESTEXCEPTION("CPipe::Read failed for the test run\n"); 370 } 371 } 372 } 373 catch(CTestException& e) 374 { 375 if(!tailString.empty()) 376 StringOut(tailString); 377 tailString.clear(); 378 StringOut(e.GetMessage()); 379 TestInfo->Log += e.GetMessage(); 380 } 381 382 /* Print what's left */ 383 if(!tailString.empty()) 384 StringOut(tailString); 385 386 TotalTime = ((float)GetTickCount() - StartTime)/1000; 387 ssFinish << "Test " << TestInfo->Test << " completed in "; 388 ssFinish << setprecision(2) << fixed << TotalTime << " seconds." << endl; 389 StringOut(ssFinish.str()); 390 TestInfo->Log += ssFinish.str(); 391} 392 393/** 394 * Interface to other classes for running all desired Wine tests. 395 */ 396void 397CWineTest::Run() 398{ 399 auto_ptr<CTestList> TestList; 400 auto_ptr<CWebService> WebService; 401 CTestInfo* TestInfo; 402 DWORD ErrorMode = 0; 403 404 /* The virtual test list is of course faster, so it should be preferred over 405 the journaled one. 406 Enable the journaled one only in case ... 407 - we're running under ReactOS (as the journal is only useful in conjunction with sysreg2) 408 - we shall keep information for Crash Recovery 409 - and the user didn't specify a module (then doing Crash Recovery doesn't really make sense) */ 410 if(Configuration.IsReactOS() && Configuration.DoCrashRecovery() && Configuration.GetModule().empty()) 411 { 412 /* Use a test list with a permanent journal */ 413 TestList.reset(new CJournaledTestList(this)); 414 } 415 else 416 { 417 /* Use the fast virtual test list with no additional overhead */ 418 TestList.reset(new CVirtualTestList(this)); 419 } 420 421 /* Initialize the Web Service interface if required */ 422 if (Configuration.DoSubmit()) 423 { 424 if (CWebServiceLibCurl::CanUseLibCurl()) 425 { 426 StringOut("[ROSAUTOTEST] Using libcurl\n"); 427 WebService.reset(new CWebServiceLibCurl()); 428 } 429 else 430 { 431 StringOut("[ROSAUTOTEST] Using wininet\n"); 432 WebService.reset(new CWebServiceWinInet()); 433 } 434 } 435 436 /* Disable error dialogs if we're running in non-interactive mode */ 437 if(!Configuration.IsInteractive()) 438 ErrorMode = SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX); 439 440 /* Get information for each test to run */ 441 while((TestInfo = TestList->GetNextTestInfo()) != 0) 442 { 443 auto_ptr<CTestInfo> TestInfoPtr(TestInfo); 444 445 RunTest(TestInfo); 446 447 if(Configuration.DoSubmit() && !TestInfo->Log.empty()) 448 WebService->Submit("wine", TestInfo); 449 450 StringOut("\n\n"); 451 } 452 453 /* We're done with all tests. Finish this run */ 454 if(Configuration.DoSubmit()) 455 WebService->Finish("wine"); 456 457 /* Restore the original error mode */ 458 if(!Configuration.IsInteractive()) 459 SetErrorMode(ErrorMode); 460}