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