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 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 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 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. 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. 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 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 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 271 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 >& 281 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 291 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 325 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 389 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 419 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 432 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& 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 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