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