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