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