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  */
CWineTest()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  */
~CWineTest()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
GetNextFile()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
DoListCommand()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
GetNextTest()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*
GetNextTestInfo()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
RunTest(CTestInfo * TestInfo)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     SetCurrentDirectoryW(m_TestPath.c_str());
302 
303     StartTime = GetTickCount();
304 
305     try
306     {
307         /* Execute the test */
308         CPipedProcess Process(TestInfo->CommandLine, Pipe);
309 
310         /* Receive all the data from the pipe */
311         for (;;)
312         {
313             DWORD dwReadResult = Pipe.Read(Buffer, sizeof(Buffer) - 1, &BytesAvailable, ProcessActivityTimeout);
314             if (dwReadResult == ERROR_SUCCESS)
315             {
316                 /* Output text through StringOut, even while the test is still running */
317                 Buffer[BytesAvailable] = 0;
318                 tailString = StringOut(tailString.append(string(Buffer)), false);
319 
320                 if (Configuration.DoSubmit())
321                     TestInfo->Log += Buffer;
322             }
323             else if (dwReadResult == ERROR_BROKEN_PIPE)
324             {
325                 // The process finished and has been terminated.
326                 break;
327             }
328             else if (dwReadResult == WAIT_TIMEOUT)
329             {
330                 // The process activity timeout above has elapsed without any new data.
331                 TESTEXCEPTION("Timeout while waiting for the test process\n");
332             }
333             else
334             {
335                 // An unexpected error.
336                 TESTEXCEPTION("CPipe::Read failed for the test run\n");
337             }
338         }
339     }
340     catch(CTestException& e)
341     {
342         if(!tailString.empty())
343             StringOut(tailString);
344         tailString.clear();
345         StringOut(e.GetMessage());
346         TestInfo->Log += e.GetMessage();
347     }
348 
349     /* Print what's left */
350     if(!tailString.empty())
351         StringOut(tailString);
352 
353     TotalTime = ((float)GetTickCount() - StartTime)/1000;
354     ssFinish << "Test " << TestInfo->Test << " completed in ";
355     ssFinish << setprecision(2) << fixed << TotalTime << " seconds." << endl;
356     StringOut(ssFinish.str());
357     TestInfo->Log += ssFinish.str();
358 }
359 
360 /**
361  * Interface to other classes for running all desired Wine tests.
362  */
363 void
Run()364 CWineTest::Run()
365 {
366     auto_ptr<CTestList> TestList;
367     auto_ptr<CWebService> WebService;
368     CTestInfo* TestInfo;
369     DWORD ErrorMode;
370 
371     /* The virtual test list is of course faster, so it should be preferred over
372        the journaled one.
373        Enable the journaled one only in case ...
374           - we're running under ReactOS (as the journal is only useful in conjunction with sysreg2)
375           - we shall keep information for Crash Recovery
376           - and the user didn't specify a module (then doing Crash Recovery doesn't really make sense) */
377     if(Configuration.IsReactOS() && Configuration.DoCrashRecovery() && Configuration.GetModule().empty())
378     {
379         /* Use a test list with a permanent journal */
380         TestList.reset(new CJournaledTestList(this));
381     }
382     else
383     {
384         /* Use the fast virtual test list with no additional overhead */
385         TestList.reset(new CVirtualTestList(this));
386     }
387 
388     /* Initialize the Web Service interface if required */
389     if(Configuration.DoSubmit())
390         WebService.reset(new CWebService());
391 
392     /* Disable error dialogs if we're running in non-interactive mode */
393     if(!Configuration.IsInteractive())
394         ErrorMode = SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX);
395 
396     /* Get information for each test to run */
397     while((TestInfo = TestList->GetNextTestInfo()) != 0)
398     {
399         auto_ptr<CTestInfo> TestInfoPtr(TestInfo);
400 
401         RunTest(TestInfo);
402 
403         if(Configuration.DoSubmit() && !TestInfo->Log.empty())
404             WebService->Submit("wine", TestInfo);
405 
406         StringOut("\n\n");
407     }
408 
409     /* We're done with all tests. Finish this run */
410     if(Configuration.DoSubmit())
411         WebService->Finish("wine");
412 
413     /* Restore the original error mode */
414     if(!Configuration.IsInteractive())
415         SetErrorMode(ErrorMode);
416 }
417