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         try
217         {
218             while(GetNextTest())
219             {
220                 /* If the user specified a test through the command line, check this here */
221                 if(!Configuration.GetTest().empty() && Configuration.GetTest() != m_CurrentTest)
222                     continue;
223 
224                 {
225                     auto_ptr<CTestInfo> TestInfo(new CTestInfo());
226                     size_t UnderscorePosition;
227 
228                     /* Build the command line */
229                     TestInfo->CommandLine = m_TestPath;
230                     TestInfo->CommandLine += m_CurrentFile;
231                     TestInfo->CommandLine += ' ';
232                     TestInfo->CommandLine += AsciiToUnicode(m_CurrentTest);
233 
234                     /* Store the Module name */
235                     UnderscorePosition = m_CurrentFile.find_last_of('_');
236 
237                     if(UnderscorePosition == m_CurrentFile.npos)
238                     {
239                         stringstream ss;
240 
241                         ss << "Invalid test file name: " << UnicodeToAscii(m_CurrentFile) << endl;
242                         SSEXCEPTION;
243                     }
244 
245                     TestInfo->Module = UnicodeToAscii(m_CurrentFile.substr(0, UnderscorePosition));
246 
247                     /* Store the test */
248                     TestInfo->Test = m_CurrentTest;
249 
250                     return TestInfo.release();
251                 }
252             }
253         }
254         catch(CTestException& e)
255         {
256             stringstream ss;
257 
258             ss << "An exception occurred trying to list tests for: " << UnicodeToAscii(m_CurrentFile) << endl;
259             StringOut(ss.str());
260             StringOut(e.GetMessage());
261             StringOut("\n");
262             m_CurrentFile.clear();
263             delete[] m_ListBuffer;
264         }
265     }
266 
267     return NULL;
268 }
269 
270 /**
271  * Runs a Wine test and captures the output
272  *
273  * @param TestInfo
274  * Pointer to a CTestInfo object containing information about the test.
275  * Will contain the test log afterwards if the user wants to submit data.
276  */
277 void
278 CWineTest::RunTest(CTestInfo* TestInfo)
279 {
280     DWORD BytesAvailable;
281     stringstream ss, ssFinish;
282     DWORD StartTime;
283     float TotalTime;
284     string tailString;
285     CPipe Pipe;
286     char Buffer[1024];
287 
288     ss << "Running Wine Test, Module: " << TestInfo->Module << ", Test: " << TestInfo->Test << endl;
289     StringOut(ss.str());
290 
291     StartTime = GetTickCount();
292 
293     try
294     {
295         /* Execute the test */
296         CPipedProcess Process(TestInfo->CommandLine, Pipe);
297 
298         /* Receive all the data from the pipe */
299         for (;;)
300         {
301             DWORD dwReadResult = Pipe.Read(Buffer, sizeof(Buffer) - 1, &BytesAvailable, ProcessActivityTimeout);
302             if (dwReadResult == ERROR_SUCCESS)
303             {
304                 /* Output text through StringOut, even while the test is still running */
305                 Buffer[BytesAvailable] = 0;
306                 tailString = StringOut(tailString.append(string(Buffer)), false);
307 
308                 if (Configuration.DoSubmit())
309                     TestInfo->Log += Buffer;
310             }
311             else if (dwReadResult == ERROR_BROKEN_PIPE)
312             {
313                 // The process finished and has been terminated.
314                 break;
315             }
316             else if (dwReadResult == WAIT_TIMEOUT)
317             {
318                 // The process activity timeout above has elapsed without any new data.
319                 TESTEXCEPTION("Timeout while waiting for the test process\n");
320             }
321             else
322             {
323                 // An unexpected error.
324                 TESTEXCEPTION("CPipe::Read failed for the test run\n");
325             }
326         }
327     }
328     catch(CTestException& e)
329     {
330         if(!tailString.empty())
331             StringOut(tailString);
332         tailString.clear();
333         StringOut(e.GetMessage());
334         TestInfo->Log += e.GetMessage();
335     }
336 
337     /* Print what's left */
338     if(!tailString.empty())
339         StringOut(tailString);
340 
341     TotalTime = ((float)GetTickCount() - StartTime)/1000;
342     ssFinish << "Test " << TestInfo->Test << " completed in ";
343     ssFinish << setprecision(2) << fixed << TotalTime << " seconds." << endl;
344     StringOut(ssFinish.str());
345     TestInfo->Log += ssFinish.str();
346 }
347 
348 /**
349  * Interface to other classes for running all desired Wine tests.
350  */
351 void
352 CWineTest::Run()
353 {
354     auto_ptr<CTestList> TestList;
355     auto_ptr<CWebService> WebService;
356     CTestInfo* TestInfo;
357     DWORD ErrorMode;
358 
359     /* The virtual test list is of course faster, so it should be preferred over
360        the journaled one.
361        Enable the journaled one only in case ...
362           - we're running under ReactOS (as the journal is only useful in conjunction with sysreg2)
363           - we shall keep information for Crash Recovery
364           - and the user didn't specify a module (then doing Crash Recovery doesn't really make sense) */
365     if(Configuration.IsReactOS() && Configuration.DoCrashRecovery() && Configuration.GetModule().empty())
366     {
367         /* Use a test list with a permanent journal */
368         TestList.reset(new CJournaledTestList(this));
369     }
370     else
371     {
372         /* Use the fast virtual test list with no additional overhead */
373         TestList.reset(new CVirtualTestList(this));
374     }
375 
376     /* Initialize the Web Service interface if required */
377     if(Configuration.DoSubmit())
378         WebService.reset(new CWebService());
379 
380     /* Disable error dialogs if we're running in non-interactive mode */
381     if(!Configuration.IsInteractive())
382         ErrorMode = SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX);
383 
384     /* Get information for each test to run */
385     while((TestInfo = TestList->GetNextTestInfo()) != 0)
386     {
387         auto_ptr<CTestInfo> TestInfoPtr(TestInfo);
388 
389         RunTest(TestInfo);
390 
391         if(Configuration.DoSubmit() && !TestInfo->Log.empty())
392             WebService->Submit("wine", TestInfo);
393 
394         StringOut("\n\n");
395     }
396 
397     /* We're done with all tests. Finish this run */
398     if(Configuration.DoSubmit())
399         WebService->Finish("wine");
400 
401     /* Restore the original error mode */
402     if(!Configuration.IsInteractive())
403         SetErrorMode(ErrorMode);
404 }
405