1 /* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
2    file Copyright.txt or https://cmake.org/licensing for details.  */
3 #include "cmCTestLaunch.h"
4 
5 #include <cstring>
6 #include <iostream>
7 
8 #include "cmsys/FStream.hxx"
9 #include "cmsys/Process.h"
10 #include "cmsys/RegularExpression.hxx"
11 
12 #include "cmCTestLaunchReporter.h"
13 #include "cmGlobalGenerator.h"
14 #include "cmMakefile.h"
15 #include "cmProcessOutput.h"
16 #include "cmState.h"
17 #include "cmStateSnapshot.h"
18 #include "cmStringAlgorithms.h"
19 #include "cmSystemTools.h"
20 #include "cmake.h"
21 
22 #ifdef _WIN32
23 #  include <fcntl.h> // for _O_BINARY
24 #  include <io.h>    // for _setmode
25 #  include <stdio.h> // for std{out,err} and fileno
26 #endif
27 
cmCTestLaunch(int argc,const char * const * argv)28 cmCTestLaunch::cmCTestLaunch(int argc, const char* const* argv)
29 {
30   this->Process = nullptr;
31 
32   if (!this->ParseArguments(argc, argv)) {
33     return;
34   }
35 
36   this->Reporter.RealArgs = this->RealArgs;
37   this->Reporter.ComputeFileNames();
38 
39   this->ScrapeRulesLoaded = false;
40   this->HaveOut = false;
41   this->HaveErr = false;
42   this->Process = cmsysProcess_New();
43 }
44 
~cmCTestLaunch()45 cmCTestLaunch::~cmCTestLaunch()
46 {
47   cmsysProcess_Delete(this->Process);
48 }
49 
ParseArguments(int argc,const char * const * argv)50 bool cmCTestLaunch::ParseArguments(int argc, const char* const* argv)
51 {
52   // Launcher options occur first and are separated from the real
53   // command line by a '--' option.
54   enum Doing
55   {
56     DoingNone,
57     DoingOutput,
58     DoingSource,
59     DoingLanguage,
60     DoingTargetName,
61     DoingTargetType,
62     DoingBuildDir,
63     DoingCount,
64     DoingFilterPrefix
65   };
66   Doing doing = DoingNone;
67   int arg0 = 0;
68   for (int i = 1; !arg0 && i < argc; ++i) {
69     const char* arg = argv[i];
70     if (strcmp(arg, "--") == 0) {
71       arg0 = i + 1;
72     } else if (strcmp(arg, "--output") == 0) {
73       doing = DoingOutput;
74     } else if (strcmp(arg, "--source") == 0) {
75       doing = DoingSource;
76     } else if (strcmp(arg, "--language") == 0) {
77       doing = DoingLanguage;
78     } else if (strcmp(arg, "--target-name") == 0) {
79       doing = DoingTargetName;
80     } else if (strcmp(arg, "--target-type") == 0) {
81       doing = DoingTargetType;
82     } else if (strcmp(arg, "--build-dir") == 0) {
83       doing = DoingBuildDir;
84     } else if (strcmp(arg, "--filter-prefix") == 0) {
85       doing = DoingFilterPrefix;
86     } else if (doing == DoingOutput) {
87       this->Reporter.OptionOutput = arg;
88       doing = DoingNone;
89     } else if (doing == DoingSource) {
90       this->Reporter.OptionSource = arg;
91       doing = DoingNone;
92     } else if (doing == DoingLanguage) {
93       this->Reporter.OptionLanguage = arg;
94       if (this->Reporter.OptionLanguage == "CXX") {
95         this->Reporter.OptionLanguage = "C++";
96       }
97       doing = DoingNone;
98     } else if (doing == DoingTargetName) {
99       this->Reporter.OptionTargetName = arg;
100       doing = DoingNone;
101     } else if (doing == DoingTargetType) {
102       this->Reporter.OptionTargetType = arg;
103       doing = DoingNone;
104     } else if (doing == DoingBuildDir) {
105       this->Reporter.OptionBuildDir = arg;
106       doing = DoingNone;
107     } else if (doing == DoingFilterPrefix) {
108       this->Reporter.OptionFilterPrefix = arg;
109       doing = DoingNone;
110     }
111   }
112 
113   // Extract the real command line.
114   if (arg0) {
115     this->RealArgC = argc - arg0;
116     this->RealArgV = argv + arg0;
117     for (int i = 0; i < this->RealArgC; ++i) {
118       this->HandleRealArg(this->RealArgV[i]);
119     }
120     return true;
121   }
122   this->RealArgC = 0;
123   this->RealArgV = nullptr;
124   std::cerr << "No launch/command separator ('--') found!\n";
125   return false;
126 }
127 
HandleRealArg(const char * arg)128 void cmCTestLaunch::HandleRealArg(const char* arg)
129 {
130 #ifdef _WIN32
131   // Expand response file arguments.
132   if (arg[0] == '@' && cmSystemTools::FileExists(arg + 1)) {
133     cmsys::ifstream fin(arg + 1);
134     std::string line;
135     while (cmSystemTools::GetLineFromStream(fin, line)) {
136       cmSystemTools::ParseWindowsCommandLine(line.c_str(), this->RealArgs);
137     }
138     return;
139   }
140 #endif
141   this->RealArgs.emplace_back(arg);
142 }
143 
RunChild()144 void cmCTestLaunch::RunChild()
145 {
146   // Ignore noopt make rules
147   if (this->RealArgs.empty() || this->RealArgs[0] == ":") {
148     this->Reporter.ExitCode = 0;
149     return;
150   }
151 
152   // Prepare to run the real command.
153   cmsysProcess* cp = this->Process;
154   cmsysProcess_SetCommand(cp, this->RealArgV);
155 
156   cmsys::ofstream fout;
157   cmsys::ofstream ferr;
158   if (this->Reporter.Passthru) {
159     // In passthru mode we just share the output pipes.
160     cmsysProcess_SetPipeShared(cp, cmsysProcess_Pipe_STDOUT, 1);
161     cmsysProcess_SetPipeShared(cp, cmsysProcess_Pipe_STDERR, 1);
162   } else {
163     // In full mode we record the child output pipes to log files.
164     fout.open(this->Reporter.LogOut.c_str(), std::ios::out | std::ios::binary);
165     ferr.open(this->Reporter.LogErr.c_str(), std::ios::out | std::ios::binary);
166   }
167 
168 #ifdef _WIN32
169   // Do this so that newline transformation is not done when writing to cout
170   // and cerr below.
171   _setmode(fileno(stdout), _O_BINARY);
172   _setmode(fileno(stderr), _O_BINARY);
173 #endif
174 
175   // Run the real command.
176   cmsysProcess_Execute(cp);
177 
178   // Record child stdout and stderr if necessary.
179   if (!this->Reporter.Passthru) {
180     char* data = nullptr;
181     int length = 0;
182     cmProcessOutput processOutput;
183     std::string strdata;
184     while (int p = cmsysProcess_WaitForData(cp, &data, &length, nullptr)) {
185       if (p == cmsysProcess_Pipe_STDOUT) {
186         processOutput.DecodeText(data, length, strdata, 1);
187         fout.write(strdata.c_str(), strdata.size());
188         std::cout.write(strdata.c_str(), strdata.size());
189         this->HaveOut = true;
190       } else if (p == cmsysProcess_Pipe_STDERR) {
191         processOutput.DecodeText(data, length, strdata, 2);
192         ferr.write(strdata.c_str(), strdata.size());
193         std::cerr.write(strdata.c_str(), strdata.size());
194         this->HaveErr = true;
195       }
196     }
197     processOutput.DecodeText(std::string(), strdata, 1);
198     if (!strdata.empty()) {
199       fout.write(strdata.c_str(), strdata.size());
200       std::cout.write(strdata.c_str(), strdata.size());
201     }
202     processOutput.DecodeText(std::string(), strdata, 2);
203     if (!strdata.empty()) {
204       ferr.write(strdata.c_str(), strdata.size());
205       std::cerr.write(strdata.c_str(), strdata.size());
206     }
207   }
208 
209   // Wait for the real command to finish.
210   cmsysProcess_WaitForExit(cp, nullptr);
211   this->Reporter.ExitCode = cmsysProcess_GetExitValue(cp);
212 }
213 
Run()214 int cmCTestLaunch::Run()
215 {
216   if (!this->Process) {
217     std::cerr << "Could not allocate cmsysProcess instance!\n";
218     return -1;
219   }
220 
221   this->RunChild();
222 
223   if (this->CheckResults()) {
224     return this->Reporter.ExitCode;
225   }
226 
227   this->LoadConfig();
228   this->Reporter.Process = this->Process;
229   this->Reporter.WriteXML();
230 
231   return this->Reporter.ExitCode;
232 }
233 
CheckResults()234 bool cmCTestLaunch::CheckResults()
235 {
236   // Skip XML in passthru mode.
237   if (this->Reporter.Passthru) {
238     return true;
239   }
240 
241   // We always report failure for error conditions.
242   if (this->Reporter.IsError()) {
243     return false;
244   }
245 
246   // Scrape the output logs to look for warnings.
247   if ((this->HaveErr && this->ScrapeLog(this->Reporter.LogErr)) ||
248       (this->HaveOut && this->ScrapeLog(this->Reporter.LogOut))) {
249     return false;
250   }
251   return true;
252 }
253 
LoadScrapeRules()254 void cmCTestLaunch::LoadScrapeRules()
255 {
256   if (this->ScrapeRulesLoaded) {
257     return;
258   }
259   this->ScrapeRulesLoaded = true;
260 
261   // Load custom match rules given to us by CTest.
262   this->LoadScrapeRules("Warning", this->Reporter.RegexWarning);
263   this->LoadScrapeRules("WarningSuppress",
264                         this->Reporter.RegexWarningSuppress);
265 }
266 
LoadScrapeRules(const char * purpose,std::vector<cmsys::RegularExpression> & regexps) const267 void cmCTestLaunch::LoadScrapeRules(
268   const char* purpose, std::vector<cmsys::RegularExpression>& regexps) const
269 {
270   std::string fname =
271     cmStrCat(this->Reporter.LogDir, "Custom", purpose, ".txt");
272   cmsys::ifstream fin(fname.c_str(), std::ios::in | std::ios::binary);
273   std::string line;
274   cmsys::RegularExpression rex;
275   while (cmSystemTools::GetLineFromStream(fin, line)) {
276     if (rex.compile(line)) {
277       regexps.push_back(rex);
278     }
279   }
280 }
281 
ScrapeLog(std::string const & fname)282 bool cmCTestLaunch::ScrapeLog(std::string const& fname)
283 {
284   this->LoadScrapeRules();
285 
286   // Look for log file lines matching warning expressions but not
287   // suppression expressions.
288   cmsys::ifstream fin(fname.c_str(), std::ios::in | std::ios::binary);
289   std::string line;
290   while (cmSystemTools::GetLineFromStream(fin, line)) {
291     if (this->Reporter.MatchesFilterPrefix(line)) {
292       continue;
293     }
294 
295     if (this->Reporter.Match(line, this->Reporter.RegexWarning) &&
296         !this->Reporter.Match(line, this->Reporter.RegexWarningSuppress)) {
297       return true;
298     }
299   }
300   return false;
301 }
302 
Main(int argc,const char * const argv[])303 int cmCTestLaunch::Main(int argc, const char* const argv[])
304 {
305   if (argc == 2) {
306     std::cerr << "ctest --launch: this mode is for internal CTest use only"
307               << std::endl;
308     return 1;
309   }
310   cmCTestLaunch self(argc, argv);
311   return self.Run();
312 }
313 
LoadConfig()314 void cmCTestLaunch::LoadConfig()
315 {
316   cmake cm(cmake::RoleScript, cmState::CTest);
317   cm.SetHomeDirectory("");
318   cm.SetHomeOutputDirectory("");
319   cm.GetCurrentSnapshot().SetDefaultDefinitions();
320   cmGlobalGenerator gg(&cm);
321   cmMakefile mf(&gg, cm.GetCurrentSnapshot());
322   std::string fname =
323     cmStrCat(this->Reporter.LogDir, "CTestLaunchConfig.cmake");
324   if (cmSystemTools::FileExists(fname) && mf.ReadListFile(fname)) {
325     this->Reporter.SourceDir = mf.GetSafeDefinition("CTEST_SOURCE_DIRECTORY");
326     cmSystemTools::ConvertToUnixSlashes(this->Reporter.SourceDir);
327   }
328 }
329