1 // Copyright 2024 The Kyua Authors.
2 // All rights reserved.
3 //
4 // Redistribution and use in source and binary forms, with or without
5 // modification, are permitted provided that the following conditions are
6 // met:
7 //
8 // * Redistributions of source code must retain the above copyright
9 // notice, this list of conditions and the following disclaimer.
10 // * Redistributions in binary form must reproduce the above copyright
11 // notice, this list of conditions and the following disclaimer in the
12 // documentation and/or other materials provided with the distribution.
13 // * Neither the name of Google Inc. nor the names of its contributors
14 // may be used to endorse or promote products derived from this software
15 // without specific prior written permission.
16 //
17 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18 // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19 // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20 // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
21 // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22 // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23 // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24 // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
25 // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
29 #include "engine/googletest_result.hpp"
30
31 #include <cstdlib>
32 #include <fstream>
33 #include <regex>
34 #include <utility>
35
36 #include "engine/exceptions.hpp"
37 #include "model/test_result.hpp"
38 #include "utils/fs/path.hpp"
39 #include "utils/format/macros.hpp"
40 #include "utils/optional.ipp"
41 #include "utils/process/status.hpp"
42 #include "utils/sanity.hpp"
43 #include "utils/text/exceptions.hpp"
44 #include "utils/text/operations.ipp"
45
46 namespace fs = utils::fs;
47 namespace process = utils::process;
48 namespace text = utils::text;
49
50 using utils::none;
51 using utils::optional;
52
53
54 namespace {
55
56 /// Internal string for specifying invalid output.
57 const std::string invalid_output_message = "invalid output";
58
59 /// Regular expression for disabled tests line.
60 const std::regex disabled_re(
61 R"RE((YOU HAVE [[:digit:]]+ DISABLED TESTS?))RE"
62 );
63
64 /// Regular expression for starting sentinel of results block.
65 const std::regex starting_sentinel_re(
66 R"(\[[[:space:]]+RUN[[:space:]]+\][[:space:]]+[A-Za-z0-9]+\.[A-Za-z0-9]+)"
67 );
68
69 /// Regular expression for ending sentinel of results block.
70 const std::regex ending_sentinel_re(
71 R"RE(\[[[:space:]]+(FAILED|OK|SKIPPED)[[:space:]]+\])RE"
72 );
73
74 /// Parses a test result that does not accept a reason.
75 ///
76 /// \param status The result status name.
77 /// \param rest The rest of the line after the status name.
78 ///
79 /// \return An object representing the test result.
80 ///
81 /// \throw format_error If the result is invalid (i.e. rest is invalid).
82 ///
83 /// \pre status must be "successful".
84 static engine::googletest_result
parse_without_reason(const std::string & status,const std::string & rest)85 parse_without_reason(const std::string& status, const std::string& rest)
86 {
87 if (!rest.empty())
88 throw engine::format_error(F("%s cannot have a reason") % status);
89
90 if (status == "skipped")
91 return engine::googletest_result(engine::googletest_result::skipped);
92 else {
93 INV(status == "successful");
94 return engine::googletest_result(engine::googletest_result::successful);
95 }
96 }
97
98
99 /// Parses a test result that needs a reason.
100 ///
101 /// \param status The result status name.
102 /// \param rest The rest of the line after the status name.
103 ///
104 /// \return An object representing the test result.
105 ///
106 /// \throw format_error If the result is invalid (i.e. rest is invalid).
107 ///
108 /// \pre status must be one of "broken", "disabled", "failed", or "skipped".
109 static engine::googletest_result
parse_with_reason(const std::string & status,const std::string & rest)110 parse_with_reason(const std::string& status, const std::string& rest)
111 {
112 using engine::googletest_result;
113
114 INV(!rest.empty());
115
116 if (status == "broken")
117 return googletest_result(googletest_result::broken, rest);
118 else if (status == "disabled")
119 return googletest_result(googletest_result::disabled, rest);
120 else if (status == "failed")
121 return googletest_result(googletest_result::failed, rest);
122 else if (status == "skipped")
123 return googletest_result(googletest_result::skipped, rest);
124 else
125 PRE_MSG(false, "Unexpected status");
126 }
127
128
129 /// Formats the termination status of a process to be used with validate_result.
130 ///
131 /// \param status The status to format.
132 ///
133 /// \return A string describing the status.
134 static std::string
format_status(const process::status & status)135 format_status(const process::status& status)
136 {
137 if (status.exited())
138 return F("exited with code %s") % status.exitstatus();
139 else if (status.signaled())
140 return F("received signal %s%s") % status.termsig() %
141 (status.coredump() ? " (core dumped)" : "");
142 else
143 return F("terminated in an unknown manner");
144 }
145
146
147 } // anonymous namespace
148
149
150 /// Constructs a raw result with a type.
151 ///
152 /// The reason is left uninitialized.
153 ///
154 /// \param type_ The type of the result.
googletest_result(const types type_)155 engine::googletest_result::googletest_result(const types type_) :
156 _type(type_)
157 {
158 }
159
160
161 /// Constructs a raw result with a type and a reason.
162 ///
163 /// \param type_ The type of the result.
164 /// \param reason_ The reason for the result.
googletest_result(const types type_,const std::string & reason_)165 engine::googletest_result::googletest_result(
166 const types type_,
167 const std::string& reason_) :
168 _type(type_), _reason(reason_)
169 {
170 }
171
172
173 /// Parses an input stream to extract a test result.
174 ///
175 /// If the parsing fails for any reason, the test result is 'broken' and it
176 /// contains the reason for the parsing failure. Test cases that report results
177 /// in an inconsistent state cannot be trusted (e.g. the test program code may
178 /// have a bug), and thus why they are reported as broken instead of just failed
179 /// (which is a legitimate result for a test case).
180 ///
181 /// \param input The stream to read from.
182 ///
183 /// \return A generic representation of the result of the test case.
184 ///
185 /// \throw format_error If the input is invalid.
186 engine::googletest_result
parse(std::istream & input)187 engine::googletest_result::parse(std::istream& input)
188 {
189 std::vector<std::string> lines;
190
191 do {
192 std::string line;
193 std::getline(input, line, '\n');
194 if (!input.eof())
195 line.push_back('\n');
196 lines.push_back(line);
197 } while (input.good());
198
199 bool capture_context = false;
200 bool valid_output = false;
201 std::string context, status;
202
203 for (auto& line: lines) {
204 std::smatch matches;
205
206 if (regex_search(line, matches, disabled_re)) {
207 context = matches[1];
208 status = "disabled";
209 valid_output = true;
210 break;
211 }
212 if (regex_search(line, matches, starting_sentinel_re)) {
213 capture_context = true;
214 context = "";
215 continue;
216 }
217 if (regex_search(line, matches, ending_sentinel_re)) {
218 std::string googletest_res = matches[1];
219 if (googletest_res == "OK") {
220 context = "";
221 status = "successful";
222 } else if (googletest_res == "FAILED") {
223 status = "failed";
224 } else {
225 INV(googletest_res == "SKIPPED");
226 status = "skipped";
227 }
228 capture_context = false;
229 valid_output = true;
230 }
231 if (capture_context) {
232 context += line;
233 }
234 }
235 if (!valid_output) {
236 context = invalid_output_message;
237 status = "broken";
238 }
239 if (status == "skipped" && context.empty())
240 context = bogus_googletest_skipped_nul_message;
241 if (context.empty())
242 return parse_without_reason(status, context);
243 else
244 return parse_with_reason(status, context);
245 }
246
247
248 /// Loads a test case result from a file.
249 ///
250 /// \param file The file to parse.
251 ///
252 /// \return The parsed test case result if all goes well.
253 ///
254 /// \throw std::runtime_error If the file does not exist.
255 /// \throw engine::format_error If the contents of the file are bogus.
256 engine::googletest_result
load(const fs::path & file)257 engine::googletest_result::load(const fs::path& file)
258 {
259 std::ifstream input(file.c_str());
260 if (!input)
261 throw std::runtime_error("Cannot open results file");
262 else
263 return parse(input);
264 }
265
266
267 /// Gets the type of the result.
268 ///
269 /// \return A result type.
270 engine::googletest_result::types
type(void) const271 engine::googletest_result::type(void) const
272 {
273 return _type;
274 }
275
276
277 /// Gets the optional reason of the result.
278 ///
279 /// \return The reason of the result if present; none otherwise.
280 const optional< std::string >&
reason(void) const281 engine::googletest_result::reason(void) const
282 {
283 return _reason;
284 }
285
286
287 /// Checks whether the result should be reported as good or not.
288 ///
289 /// \return True if the result can be considered "good", false otherwise.
290 bool
good(void) const291 engine::googletest_result::good(void) const
292 {
293 switch (_type) {
294 case googletest_result::disabled:
295 case googletest_result::skipped:
296 case googletest_result::successful:
297 return true;
298
299 case googletest_result::broken:
300 case googletest_result::failed:
301 return false;
302
303 default:
304 UNREACHABLE;
305 }
306 }
307
308
309 /// Reinterprets a raw result based on the termination status of the test case.
310 ///
311 /// This reinterpretation ensures that the termination conditions of the program
312 /// match what is expected of the paticular result reported by the test program.
313 /// If such conditions do not match, the test program is considered bogus and is
314 /// thus reported as broken.
315 ///
316 /// This is just a helper function for calculate_result(); the real result of
317 /// the test case cannot be inferred from apply() only.
318 ///
319 /// \param status The exit status of the test program, or none if the test
320 /// program timed out.
321 ///
322 /// \result The adjusted result. The original result is transformed into broken
323 /// if the exit status of the program does not match our expectations.
324 engine::googletest_result
apply(const optional<process::status> & status) const325 engine::googletest_result::apply(const optional< process::status >& status)
326 const
327 {
328 if (!status) {
329 return *this;
330 }
331
332 auto check_status = [&status](bool expect_pass) -> bool {
333 if (!status.get().exited()) {
334 return false;
335 }
336 auto exit_status = status.get().exitstatus();
337 if (expect_pass) {
338 return exit_status == EXIT_SUCCESS;
339 } else {
340 return exit_status == EXIT_FAILURE;
341 }
342 };
343
344 switch (_type) {
345 case googletest_result::broken:
346 return *this;
347
348 case googletest_result::disabled:
349 if (check_status(true)) {
350 return *this;
351 }
352 return googletest_result(googletest_result::broken,
353 "Disabled test case should have reported success but " +
354 format_status(status.get()));
355
356 case googletest_result::failed:
357 if (check_status(false)) {
358 return *this;
359 }
360 return googletest_result(googletest_result::broken,
361 "Failed test case should have reported failure but " +
362 format_status(status.get()));
363
364 case googletest_result::skipped:
365 if (check_status(true)) {
366 return *this;
367 }
368 return googletest_result(googletest_result::broken,
369 "Skipped test case should have reported success but " +
370 format_status(status.get()));
371
372 case googletest_result::successful:
373 if (check_status(true)) {
374 return *this;
375 }
376 return googletest_result(googletest_result::broken,
377 "Passed test case should have reported success but " +
378 format_status(status.get()));
379 }
380
381 UNREACHABLE;
382 }
383
384
385 /// Converts an internal result to the interface-agnostic representation.
386 ///
387 /// \return A generic result instance representing this result.
388 model::test_result
externalize(void) const389 engine::googletest_result::externalize(void) const
390 {
391 switch (_type) {
392 case googletest_result::broken:
393 return model::test_result(model::test_result_broken, _reason.get());
394
395 case googletest_result::disabled:
396 return model::test_result(model::test_result_skipped, _reason.get());
397
398 case googletest_result::failed:
399 return model::test_result(model::test_result_failed, _reason.get());
400
401 case googletest_result::skipped:
402 return model::test_result(model::test_result_skipped, _reason.get());
403
404 case googletest_result::successful:
405 return model::test_result(model::test_result_passed);
406
407 default:
408 UNREACHABLE;
409 }
410 }
411
412
413 /// Compares two raw results for equality.
414 ///
415 /// \param other The result to compare to.
416 ///
417 /// \return True if the two raw results are equal; false otherwise.
418 bool
operator ==(const googletest_result & other) const419 engine::googletest_result::operator==(const googletest_result& other) const
420 {
421 return _type == other._type &&
422 _reason == other._reason;
423 }
424
425
426 /// Compares two raw results for inequality.
427 ///
428 /// \param other The result to compare to.
429 ///
430 /// \return True if the two raw results are different; false otherwise.
431 bool
operator !=(const googletest_result & other) const432 engine::googletest_result::operator!=(const googletest_result& other) const
433 {
434 return !(*this == other);
435 }
436
437
438 /// Injects the object into a stream.
439 ///
440 /// \param output The stream into which to inject the object.
441 /// \param object The object to format.
442 ///
443 /// \return The output stream.
444 std::ostream&
operator <<(std::ostream & output,const googletest_result & object)445 engine::operator<<(std::ostream& output, const googletest_result& object)
446 {
447 std::string result_name;
448 switch (object.type()) {
449 case googletest_result::broken: result_name = "broken"; break;
450 case googletest_result::disabled: result_name = "disabled"; break;
451 case googletest_result::failed: result_name = "failed"; break;
452 case googletest_result::skipped: result_name = "skipped"; break;
453 case googletest_result::successful: result_name = "successful"; break;
454 }
455
456 const optional< std::string >& reason = object.reason();
457
458 output << F("model::test_result{type=%s, reason=%s}")
459 % text::quote(result_name, '\'')
460 % (reason ? text::quote(reason.get(), '\'') : "none");
461
462 return output;
463 }
464
465
466 /// Calculates the user-visible result of a test case.
467 ///
468 /// This function needs to perform magic to ensure that what the test case
469 /// reports as its result is what the user should really see: i.e. it adjusts
470 /// the reported status of the test to the exit conditions of its body and
471 /// cleanup parts.
472 ///
473 /// \param body_status The termination status of the process that executed
474 /// the body of the test. None if the body timed out.
475 /// \param results_file The path to the results file that the test case body is
476 /// supposed to have created.
477 ///
478 /// \return The calculated test case result.
479 model::test_result
calculate_googletest_result(const optional<process::status> & body_status,const fs::path & results_file)480 engine::calculate_googletest_result(
481 const optional< process::status >& body_status,
482 const fs::path& results_file)
483 {
484 using engine::googletest_result;
485
486 googletest_result result(googletest_result::broken, "Unknown result");
487 try {
488 result = googletest_result::load(results_file);
489 /// atf_result.cpp handles this by side-effect by setting a broken
490 /// result with "", which results in a std::runtime_error throw from
491 /// atf_result::parse(..). googletest_result::parse doesn't do that and
492 /// instead returns a broken result with a reason of
493 /// `invalid_output_message`, so that case needs to be tested for after
494 /// the fact to make sure the reason why `invalid_output_message` was
495 /// because of a program that crashed or failed.
496 if (result.type() == googletest_result::broken && body_status) {
497 const optional< std::string >& reason = result.reason();
498 if (reason && reason.get() == invalid_output_message) {
499 result = googletest_result(
500 googletest_result::broken,
501 F("Error: Premature exit. Test case %s") %
502 format_status(body_status.get()));
503 }
504 }
505 } catch (const engine::format_error& error) {
506 if (body_status)
507 result = googletest_result(googletest_result::broken,
508 F("Error: %s. Test case %s") %
509 error.what() %
510 format_status(body_status.get()));
511 else {
512 // The test case timed out. apply() handles this case later.
513 }
514 } catch (const std::runtime_error& error) {
515 if (body_status)
516 result = googletest_result(
517 googletest_result::broken,
518 F("Error: Premature exit. Test case %s") %
519 format_status(body_status.get()));
520 else {
521 // The test case timed out. apply() handles this case later.
522 }
523 }
524
525 result = result.apply(body_status);
526
527 return result.externalize();
528 }
529