xref: /freebsd/contrib/kyua/engine/googletest_helpers.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 extern "C" {
30 #include <sys/stat.h>
31 
32 #include <getopt.h>
33 #include <signal.h>
34 #include <unistd.h>
35 
36 extern char** environ;
37 }
38 
39 #include <cstdlib>
40 #include <cstring>
41 #include <fstream>
42 #include <iostream>
43 
44 #include "utils/env.hpp"
45 #include "utils/cmdline/options.hpp"
46 #include "utils/cmdline/parser.ipp"
47 #include "utils/format/containers.ipp"
48 #include "utils/format/macros.hpp"
49 #include "utils/fs/path.hpp"
50 #include "utils/optional.ipp"
51 #include "utils/test_utils.ipp"
52 
53 namespace cmdline = utils::cmdline;
54 namespace fs = utils::fs;
55 
56 using cmdline::base_option;
57 using cmdline::bool_option;
58 using cmdline::parse;
59 using cmdline::parsed_cmdline;
60 using cmdline::string_option;
61 
62 namespace fs = utils::fs;
63 
64 namespace {
65 
66 
67 /// Prefix for all testcases.
68 const char *test_suite = "Suite.";
69 
70 
71 /// Logs an error message and exits the test with an error code.
72 ///
73 /// \param str The error message to log.
74 static void
75 fail(const std::string& str)
76 {
77     std::cerr << str << '\n';
78     std::exit(EXIT_FAILURE);
79 }
80 
81 
82 /// A test scenario that validates the TEST_ENV_* variables.
83 static void
84 test_check_configuration_variables(void)
85 {
86     std::set< std::string > vars;
87     char** iter;
88     for (iter = environ; *iter != NULL; ++iter) {
89         if (std::strstr(*iter, "TEST_ENV_") == *iter) {
90             vars.insert(*iter);
91         }
92     }
93 
94     std::set< std::string > exp_vars{
95         "TEST_ENV_first=some value",
96         "TEST_ENV_second=some other value"
97     };
98 
99     if (vars == exp_vars) {
100         std::cout <<
101 "Note: Google Test filter = Suite.check_configuration_variables\n"
102 "[==========] Running 1 test from 1 test case.\n"
103 "[----------] Global test environment set-up.\n"
104 "[----------] 1 test from Suite\n"
105 "[ RUN      ] Suite.check_configuration_variables\n"
106 "[       OK ] Suite.check_configuration_variables (0 ms)\n"
107 "[----------] 1 test from PassFailTest (0 ms total)\n"
108 "\n"
109 "[----------] Global test environment tear-down\n"
110 "[==========] 1 test from 1 test case ran. (1 ms total)\n"
111 "[  PASSED  ] 1 test.\n"
112         ;
113     } else {
114         std::cout <<
115 "Note: Google Test filter = Suite.Fails\n"
116 "[==========] Running 1 test from 1 test case.\n"
117 "[----------] Global test environment set-up.\n"
118 "[----------] 1 test from PassFailTest\n"
119 "[ RUN      ] Suite.check_configuration_variables\n"
120 "pass_fail_demo.cc:12: Failure\n"
121 "Expected equality of these values:\n"
122 "  false\n"
123 "  true\n"
124 "[  FAILED  ] Suite.check_configuration_variables (0 ms)\n"
125 "[----------] 1 test from Suite (0 ms total)\n"
126 "\n"
127 "[----------] Global test environment tear-down\n"
128 "[==========] 1 test from 1 test case ran. (0 ms total)\n"
129 "[  PASSED  ] 0 tests.\n"
130 "[  FAILED  ] 1 test, listed below:\n"
131 "[  FAILED  ] Suite.check_configuration_variables\n"
132 "\n"
133 " 1 FAILED TEST\n"
134         << F("    Expected: %s\nFound: %s\n") % exp_vars % vars;
135         std::exit(EXIT_FAILURE);
136     }
137 }
138 
139 
140 /// A test scenario that triggers a crash via abort in order to generate a
141 /// core dump.
142 static void
143 test_crash(void)
144 {
145     std::abort();
146 }
147 
148 
149 /// A test scenario that reports some tests as failed.
150 static void
151 test_fail(void)
152 {
153     std::cout << (
154 "Note: Google Test filter = Suite.fail\n"
155 "[==========] Running 1 test from 1 test suite.\n"
156 "[----------] Global test environment set-up.\n"
157 "[----------] 1 test from Suite\n"
158 "[ RUN      ] Suite.fail\n"
159 "gtest_macros_demo.cc:4: Failure\n"
160 "Failed\n"
161 "with a reason\n"
162 "[  FAILED  ] Suite.fail (0 ms)\n"
163 "[----------] 1 test from Suite (0 ms total)\n"
164 "\n"
165 "[----------] Global test environment tear-down\n"
166 "[==========] 1 test from 1 test suite ran. (0 ms total)\n"
167 "[  PASSED  ] 0 tests.\n"
168 "[  FAILED  ] 1 test, listed below:\n"
169 "[  FAILED  ] Suite.fail\n"
170 "\n"
171 " 1 FAILED TEST\n"
172     );
173     std::exit(EXIT_FAILURE);
174 }
175 
176 
177 /// A test scenario that passes.
178 static void
179 test_pass(void)
180 {
181     std::cout << (
182 "Note: Google Test filter = Suite.pass\n"
183 "[==========] Running 1 test from 1 test suite.\n"
184 "[----------] Global test environment set-up.\n"
185 "[----------] 1 test from Suite\n"
186 "[ RUN      ] Suite.pass\n"
187 "[       OK ] Suite.pass (0 ms)\n"
188 "[----------] 1 test from Suite (0 ms total)\n"
189 "\n"
190 "[----------] Global test environment tear-down\n"
191 "[==========] 1 test from 1 test suite ran. (0 ms total)\n"
192 "[  PASSED  ] 1 test.\n"
193     );
194 }
195 
196 
197 /// A test scenario that passes but then exits with non-zero.
198 static void
199 test_pass_but_exit_failure(void)
200 {
201     std::cout << (
202 "Note: Google Test filter = Suite.pass_but_exit_failure\n"
203 "[==========] Running 1 test from 1 test suite.\n"
204 "[----------] Global test environment set-up.\n"
205 "[----------] 1 test from Suite\n"
206 "[ RUN      ] Suite.pass_but_exit_failure\n"
207 "[       OK ] Suite.pass_but_exit_failure (0 ms)\n"
208 "[----------] 1 test from Suite (0 ms total)\n"
209 "\n"
210 "[----------] Global test environment tear-down\n"
211 "[==========] 1 test from 1 test suite ran. (0 ms total)\n"
212 "[  PASSED  ] 1 test.\n"
213     );
214 
215     std::exit(70);
216 }
217 
218 
219 /// A test scenario that times out.
220 ///
221 /// Note that the timeout is defined in the Kyuafile, as the TAP interface has
222 /// no means for test programs to specify this by themselves.
223 static void
224 test_timeout(void)
225 {
226     std::cout << (
227 "Note: Google Test filter = Suite.incomplete\n"
228 "[==========] Running 1 test from 1 test suite.\n"
229 "[----------] Global test environment set-up.\n"
230 "[----------] 1 test from Suite\n"
231 "[ RUN      ] Suite.incomplete\n"
232     );
233 
234     ::sleep(10);
235     const fs::path control_dir = fs::path(utils::getenv("CONTROL_DIR").get());
236     std::ofstream file((control_dir / "cookie").c_str());
237     if (!file)
238         fail("Failed to create the control cookie");
239     file.close();
240 }
241 
242 
243 /// Prints out program usage and exits with a non-zero exit code.
244 static void
245 usage(const char* argv0) {
246     std::cout << "usage: " << argv0 << " "
247               << "[--gtest_color=(auto|yes|no)] "
248               << "[--gtest_filter=POSITIVE_PATTERNS] "
249               << "[--gtest_list_tests]"
250               << "\n\n"
251               << "This program mocks a googletest test program."
252               << "\n";
253     std::exit(EXIT_FAILURE);
254 }
255 
256 
257 }  // anonymous namespace
258 
259 
260 /// Entry point to the test program.
261 ///
262 /// The caller can select which test scenario to run by modifying the program's
263 /// basename on disk (either by a copy or by a hard link).
264 ///
265 /// \todo It may be worth to split this binary into separate, smaller binaries,
266 /// one for every "test scenario".  We use this program as a dispatcher for
267 /// different "main"s, the only reason being to keep the amount of helper test
268 /// programs to a minimum.  However, putting this each function in its own
269 /// binary could simplify many other things.
270 ///
271 /// \param argc The number of CLI arguments.
272 /// \param argv The CLI arguments themselves.  These are not used because
273 ///     Kyua will not pass any arguments to the plain test program.
274 int
275 main(int argc, char** argv)
276 {
277     using scenario_fn_t = void (*)(void);
278 
279     char *argv0 = argv[0];
280 
281     std::map<std::string, scenario_fn_t> scenarios{
282         { "check_configuration_variables", test_check_configuration_variables },
283         { "crash", test_crash },
284         { "fail", test_fail },
285         { "pass", test_pass },
286         { "pass_but_exit_failure", test_pass_but_exit_failure },
287         { "timeout", test_timeout }
288     };
289 
290     const bool_option gtest_list_tests_opt(
291         "gtest_list_tests", "List tests");
292     const string_option gtest_color_opt(
293         "gtest_color", "Enable/disable color support", "auto");
294     const string_option gtest_filter_opt(
295         "gtest_filter", "", "POSITIVE_PATTERNS");
296 
297     std::vector<const base_option*> options;
298     /// Ignore `--gtest_color=*`.
299     options.push_back(&gtest_color_opt);
300     options.push_back(&gtest_filter_opt);
301     options.push_back(&gtest_list_tests_opt);
302 
303     const parsed_cmdline cmdline = parse(argc, argv, options);
304     INV(cmdline.arguments().empty());
305 
306     if (cmdline.has_option("gtest_list_tests")) {
307         std::cout << test_suite << "\n";
308         for (auto it = scenarios.begin(); it != scenarios.end(); it++) {
309             std::cout << "  " << it->first << "\n";
310         }
311         return EXIT_SUCCESS;
312     }
313 
314     INV(cmdline.has_option("gtest_filter"));
315 
316     auto gtest_filter_arg = cmdline.get_option<string_option>("gtest_filter");
317     INV(gtest_filter_arg.find_first_of(test_suite) == 0);
318     auto testcase = gtest_filter_arg.erase(0, strlen(test_suite));
319 
320     auto scenario = scenarios.find(testcase);
321     if (scenario == scenarios.end()) {
322         /// Mimic googletest test programs by printing out a usage message when
323         /// a test cannot be found.
324         usage(argv0);
325     }
326 
327     scenario->second();
328 
329     return EXIT_SUCCESS;
330 }
331