xref: /freebsd/contrib/kyua/engine/googletest_result.cpp (revision eb8f69902f7de8012a31a85db2f88a54584f81af)
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