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 10 static 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. 14 static const DWORD ProcessActivityTimeout = 170000; 15 16 17 /** 18 * Constructs a CWineTest object. 19 */ 20 CWineTest::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 */ 45 CWineTest::~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 */ 60 bool 61 CWineTest::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 FoundFile = true; 72 } 73 else 74 { 75 /* Start searching for test files */ 76 wstring FindPath = m_TestPath; 77 78 /* Did the user specify a module? */ 79 if(Configuration.GetModule().empty()) 80 { 81 /* No module, so search for all files in that directory */ 82 FindPath += L"*.exe"; 83 } 84 else 85 { 86 /* Search for files with the pattern "modulename_*" */ 87 FindPath += Configuration.GetModule(); 88 FindPath += L"_*.exe"; 89 } 90 91 /* Search for the first file and check whether we got one */ 92 m_hFind = FindFirstFileW(FindPath.c_str(), &fd); 93 94 if(m_hFind != INVALID_HANDLE_VALUE) 95 FoundFile = true; 96 } 97 98 if(FoundFile) 99 m_CurrentFile = fd.cFileName; 100 101 return FoundFile; 102 } 103 104 /** 105 * Executes the --list command of a module test file to get information about the available tests. 106 * 107 * @return 108 * The number of bytes we read into the m_ListBuffer member variable by capturing the output of the --list command. 109 */ 110 DWORD 111 CWineTest::DoListCommand() 112 { 113 DWORD BytesAvailable; 114 DWORD Temp; 115 wstring CommandLine; 116 CPipe Pipe; 117 118 /* Build the command line */ 119 CommandLine = m_TestPath; 120 CommandLine += m_CurrentFile; 121 CommandLine += L" --list"; 122 123 { 124 /* Start the process for getting all available tests */ 125 CPipedProcess Process(CommandLine, Pipe); 126 127 /* Wait till this process ended */ 128 if(WaitForSingleObject(Process.GetProcessHandle(), ListTimeout) == WAIT_FAILED) 129 TESTEXCEPTION("WaitForSingleObject failed for the test list\n"); 130 } 131 132 /* Read the output data into a buffer */ 133 if(!Pipe.Peek(NULL, 0, NULL, &BytesAvailable)) 134 TESTEXCEPTION("CPipe::Peek failed for the test list\n"); 135 136 /* Check if we got any */ 137 if(!BytesAvailable) 138 { 139 stringstream ss; 140 141 ss << "The --list command did not return any data for " << UnicodeToAscii(m_CurrentFile) << endl; 142 TESTEXCEPTION(ss.str()); 143 } 144 145 /* Read the data */ 146 m_ListBuffer = new char[BytesAvailable]; 147 148 if(Pipe.Read(m_ListBuffer, BytesAvailable, &Temp, INFINITE) != ERROR_SUCCESS) 149 TESTEXCEPTION("CPipe::Read failed\n"); 150 151 return BytesAvailable; 152 } 153 154 /** 155 * Gets the next test from m_ListBuffer, which was filled with information from the --list command. 156 * 157 * @return 158 * true if a next test was found, otherwise false. 159 */ 160 bool 161 CWineTest::GetNextTest() 162 { 163 PCHAR pEnd; 164 static DWORD BufferSize; 165 static PCHAR pStart; 166 167 if(!m_ListBuffer) 168 { 169 /* Perform the --list command */ 170 BufferSize = DoListCommand(); 171 172 /* Move the pointer to the first test */ 173 pStart = strchr(m_ListBuffer, '\n'); 174 pStart += 5; 175 } 176 177 /* If we reach the buffer size, we finished analyzing the output of this test */ 178 if(pStart >= (m_ListBuffer + BufferSize)) 179 { 180 /* Clear m_CurrentFile to indicate that */ 181 m_CurrentFile.clear(); 182 183 /* Also free the memory for the list buffer */ 184 delete[] m_ListBuffer; 185 m_ListBuffer = NULL; 186 187 return false; 188 } 189 190 /* Get start and end of this test name */ 191 pEnd = pStart; 192 193 while(*pEnd != '\r') 194 ++pEnd; 195 196 /* Store the test name */ 197 m_CurrentTest = string(pStart, pEnd); 198 199 /* Move the pointer to the next test */ 200 pStart = pEnd + 6; 201 202 return true; 203 } 204 205 /** 206 * Interface to CTestList-derived classes for getting all information about the next test to be run. 207 * 208 * @return 209 * Returns a pointer to a CTestInfo object containing all available information about the next test. 210 */ 211 CTestInfo* 212 CWineTest::GetNextTestInfo() 213 { 214 while(!m_CurrentFile.empty() || GetNextFile()) 215 { 216 /* The user asked for a list of all modules */ 217 if (Configuration.ListModulesOnly()) 218 { 219 std::stringstream ss; 220 ss << "Module: " << UnicodeToAscii(m_CurrentFile) << endl; 221 m_CurrentFile.clear(); 222 StringOut(ss.str()); 223 continue; 224 } 225 226 try 227 { 228 while(GetNextTest()) 229 { 230 /* If the user specified a test through the command line, check this here */ 231 if(!Configuration.GetTest().empty() && Configuration.GetTest() != m_CurrentTest) 232 continue; 233 234 { 235 auto_ptr<CTestInfo> TestInfo(new CTestInfo()); 236 size_t UnderscorePosition; 237 238 /* Build the command line */ 239 TestInfo->CommandLine = m_TestPath; 240 TestInfo->CommandLine += m_CurrentFile; 241 TestInfo->CommandLine += ' '; 242 TestInfo->CommandLine += AsciiToUnicode(m_CurrentTest); 243 244 /* Store the Module name */ 245 UnderscorePosition = m_CurrentFile.find_last_of('_'); 246 247 if(UnderscorePosition == m_CurrentFile.npos) 248 { 249 stringstream ss; 250 251 ss << "Invalid test file name: " << UnicodeToAscii(m_CurrentFile) << endl; 252 SSEXCEPTION; 253 } 254 255 TestInfo->Module = UnicodeToAscii(m_CurrentFile.substr(0, UnderscorePosition)); 256 257 /* Store the test */ 258 TestInfo->Test = m_CurrentTest; 259 260 return TestInfo.release(); 261 } 262 } 263 } 264 catch(CTestException& e) 265 { 266 stringstream ss; 267 268 ss << "An exception occurred trying to list tests for: " << UnicodeToAscii(m_CurrentFile) << endl; 269 StringOut(ss.str()); 270 StringOut(e.GetMessage()); 271 StringOut("\n"); 272 m_CurrentFile.clear(); 273 delete[] m_ListBuffer; 274 } 275 } 276 277 return NULL; 278 } 279 280 /** 281 * Runs a Wine test and captures the output 282 * 283 * @param TestInfo 284 * Pointer to a CTestInfo object containing information about the test. 285 * Will contain the test log afterwards if the user wants to submit data. 286 */ 287 void 288 CWineTest::RunTest(CTestInfo* TestInfo) 289 { 290 DWORD BytesAvailable; 291 stringstream ss, ssFinish; 292 DWORD StartTime; 293 float TotalTime; 294 string tailString; 295 CPipe Pipe; 296 char Buffer[1024]; 297 298 ss << "Running Wine Test, Module: " << TestInfo->Module << ", Test: " << TestInfo->Test << endl; 299 StringOut(ss.str()); 300 301 StartTime = GetTickCount(); 302 303 try 304 { 305 /* Execute the test */ 306 CPipedProcess Process(TestInfo->CommandLine, Pipe); 307 308 /* Receive all the data from the pipe */ 309 for (;;) 310 { 311 DWORD dwReadResult = Pipe.Read(Buffer, sizeof(Buffer) - 1, &BytesAvailable, ProcessActivityTimeout); 312 if (dwReadResult == ERROR_SUCCESS) 313 { 314 /* Output text through StringOut, even while the test is still running */ 315 Buffer[BytesAvailable] = 0; 316 tailString = StringOut(tailString.append(string(Buffer)), false); 317 318 if (Configuration.DoSubmit()) 319 TestInfo->Log += Buffer; 320 } 321 else if (dwReadResult == ERROR_BROKEN_PIPE) 322 { 323 // The process finished and has been terminated. 324 break; 325 } 326 else if (dwReadResult == WAIT_TIMEOUT) 327 { 328 // The process activity timeout above has elapsed without any new data. 329 TESTEXCEPTION("Timeout while waiting for the test process\n"); 330 } 331 else 332 { 333 // An unexpected error. 334 TESTEXCEPTION("CPipe::Read failed for the test run\n"); 335 } 336 } 337 } 338 catch(CTestException& e) 339 { 340 if(!tailString.empty()) 341 StringOut(tailString); 342 tailString.clear(); 343 StringOut(e.GetMessage()); 344 TestInfo->Log += e.GetMessage(); 345 } 346 347 /* Print what's left */ 348 if(!tailString.empty()) 349 StringOut(tailString); 350 351 TotalTime = ((float)GetTickCount() - StartTime)/1000; 352 ssFinish << "Test " << TestInfo->Test << " completed in "; 353 ssFinish << setprecision(2) << fixed << TotalTime << " seconds." << endl; 354 StringOut(ssFinish.str()); 355 TestInfo->Log += ssFinish.str(); 356 } 357 358 /** 359 * Interface to other classes for running all desired Wine tests. 360 */ 361 void 362 CWineTest::Run() 363 { 364 auto_ptr<CTestList> TestList; 365 auto_ptr<CWebService> WebService; 366 CTestInfo* TestInfo; 367 DWORD ErrorMode; 368 369 /* The virtual test list is of course faster, so it should be preferred over 370 the journaled one. 371 Enable the journaled one only in case ... 372 - we're running under ReactOS (as the journal is only useful in conjunction with sysreg2) 373 - we shall keep information for Crash Recovery 374 - and the user didn't specify a module (then doing Crash Recovery doesn't really make sense) */ 375 if(Configuration.IsReactOS() && Configuration.DoCrashRecovery() && Configuration.GetModule().empty()) 376 { 377 /* Use a test list with a permanent journal */ 378 TestList.reset(new CJournaledTestList(this)); 379 } 380 else 381 { 382 /* Use the fast virtual test list with no additional overhead */ 383 TestList.reset(new CVirtualTestList(this)); 384 } 385 386 /* Initialize the Web Service interface if required */ 387 if(Configuration.DoSubmit()) 388 WebService.reset(new CWebService()); 389 390 /* Disable error dialogs if we're running in non-interactive mode */ 391 if(!Configuration.IsInteractive()) 392 ErrorMode = SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX); 393 394 /* Get information for each test to run */ 395 while((TestInfo = TestList->GetNextTestInfo()) != 0) 396 { 397 auto_ptr<CTestInfo> TestInfoPtr(TestInfo); 398 399 RunTest(TestInfo); 400 401 if(Configuration.DoSubmit() && !TestInfo->Log.empty()) 402 WebService->Submit("wine", TestInfo); 403 404 StringOut("\n\n"); 405 } 406 407 /* We're done with all tests. Finish this run */ 408 if(Configuration.DoSubmit()) 409 WebService->Finish("wine"); 410 411 /* Restore the original error mode */ 412 if(!Configuration.IsInteractive()) 413 SetErrorMode(ErrorMode); 414 } 415