atf-run.cpp revision 1.6 1 //
2 // Automated Testing Framework (atf)
3 //
4 // Copyright (c) 2007 The NetBSD Foundation, Inc.
5 // All rights reserved.
6 //
7 // Redistribution and use in source and binary forms, with or without
8 // modification, are permitted provided that the following conditions
9 // are met:
10 // 1. Redistributions of source code must retain the above copyright
11 // notice, this list of conditions and the following disclaimer.
12 // 2. Redistributions in binary form must reproduce the above copyright
13 // notice, this list of conditions and the following disclaimer in the
14 // documentation and/or other materials provided with the distribution.
15 //
16 // THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND
17 // CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
18 // INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
19 // MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
20 // IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS BE LIABLE FOR ANY
21 // DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
22 // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
23 // GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24 // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
25 // IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
26 // OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
27 // IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 //
29
30 extern "C" {
31 #include <sys/types.h>
32 #include <sys/param.h>
33 #include <sys/stat.h>
34 #include <sys/wait.h>
35 #include <unistd.h>
36 }
37
38 #include <algorithm>
39 #include <cassert>
40 #include <cerrno>
41 #include <cstdlib>
42 #include <cstring>
43 #include <fstream>
44 #include <iostream>
45 #include <map>
46 #include <string>
47
48 #include "application.hpp"
49 #include "atffile.hpp"
50 #include "config.hpp"
51 #include "config_file.hpp"
52 #include "env.hpp"
53 #include "exceptions.hpp"
54 #include "fs.hpp"
55 #include "parser.hpp"
56 #include "process.hpp"
57 #include "requirements.hpp"
58 #include "test-program.hpp"
59 #include "text.hpp"
60
61 namespace {
62
63 typedef std::map< std::string, std::string > vars_map;
64
65 } // anonymous namespace
66
67 class atf_run : public tools::application::app {
68 static const char* m_description;
69
70 vars_map m_cmdline_vars;
71
72 static vars_map::value_type parse_var(const std::string&);
73
74 void process_option(int, const char*);
75 std::string specific_args(void) const;
76 options_set specific_options(void) const;
77
78 void parse_vflag(const std::string&);
79
80 std::vector< std::string > conf_args(void) const;
81
82 size_t count_tps(std::vector< std::string >) const;
83
84 int run_test(const tools::fs::path&, const std::string &,
85 tools::test_program::atf_tps_writer&,
86 const vars_map&);
87 int run_test_directory(const tools::fs::path&,
88 tools::test_program::atf_tps_writer&);
89 int run_test_program(const tools::fs::path&,
90 const std::string tc,
91 tools::test_program::atf_tps_writer&,
92 const vars_map&);
93
94 tools::test_program::test_case_result get_test_case_result(
95 const std::string&, const tools::process::status&,
96 const tools::fs::path&) const;
97
98 public:
99 atf_run(void);
100
101 int main(void);
102 };
103
104 static void
105 sanitize_gdb_env(void)
106 {
107 try {
108 tools::env::unset("TERM");
109 } catch (...) {
110 // Just swallow exceptions here; they cannot propagate into C, which
111 // is where this function is called from, and even if these exceptions
112 // appear they are benign.
113 }
114 }
115
116 static void
117 dump_stacktrace(const tools::fs::path& tp, const tools::process::status& s,
118 const tools::fs::path& workdir,
119 tools::test_program::atf_tps_writer& w)
120 {
121 assert(s.signaled() && s.coredump());
122
123 w.stderr_tc("Test program crashed; attempting to get stack trace");
124
125 const tools::fs::path corename = workdir /
126 (tp.leaf_name().substr(0, MAXCOMLEN) + ".core");
127 if (!tools::fs::exists(corename)) {
128 w.stderr_tc("Expected file " + corename.str() + " not found");
129 return;
130 }
131
132 const tools::fs::path gdb(GDB);
133 const tools::fs::path gdbout = workdir / "gdb.out";
134 const tools::process::argv_array args(gdb.leaf_name().c_str(), "-batch",
135 "-q", "-ex", "bt", tp.c_str(),
136 corename.c_str(), NULL);
137 tools::process::status status = tools::process::exec(
138 gdb, args,
139 tools::process::stream_redirect_path(gdbout),
140 tools::process::stream_redirect_path(tools::fs::path("/dev/null")),
141 sanitize_gdb_env);
142 if (!status.exited() || status.exitstatus() != EXIT_SUCCESS) {
143 w.stderr_tc("Execution of " GDB " failed");
144 return;
145 }
146
147 std::ifstream input(gdbout.c_str());
148 if (input) {
149 std::string line;
150 while (std::getline(input, line).good())
151 w.stderr_tc(line);
152 input.close();
153 }
154
155 w.stderr_tc("Stack trace complete");
156 }
157
158 const char* atf_run::m_description =
159 "atf-run is a tool that runs tests programs and collects their "
160 "results.";
161
162 atf_run::atf_run(void) :
163 app(m_description, "atf-run(1)", "atf(7)")
164 {
165 }
166
167 void
168 atf_run::process_option(int ch, const char* arg)
169 {
170 switch (ch) {
171 case 'v':
172 parse_vflag(arg);
173 break;
174
175 default:
176 std::abort();
177 }
178 }
179
180 std::string
181 atf_run::specific_args(void)
182 const
183 {
184 return "[test1 .. testN]";
185 }
186
187 atf_run::options_set
188 atf_run::specific_options(void)
189 const
190 {
191 using tools::application::option;
192 options_set opts;
193 opts.insert(option('v', "var=value", "Sets the configuration variable "
194 "`var' to `value'; overrides "
195 "values in configuration files"));
196 return opts;
197 }
198
199 void
200 atf_run::parse_vflag(const std::string& str)
201 {
202 if (str.empty())
203 throw std::runtime_error("-v requires a non-empty argument");
204
205 std::vector< std::string > ws = tools::text::split(str, "=");
206 if (ws.size() == 1 && str[str.length() - 1] == '=') {
207 m_cmdline_vars[ws[0]] = "";
208 } else {
209 if (ws.size() != 2)
210 throw std::runtime_error("-v requires an argument of the form "
211 "var=value");
212
213 m_cmdline_vars[ws[0]] = ws[1];
214 }
215 }
216
217 int
218 atf_run::run_test(const tools::fs::path& tp,
219 const std::string &tc,
220 tools::test_program::atf_tps_writer& w,
221 const vars_map& config)
222 {
223 tools::fs::file_info fi(tp);
224
225 int errcode;
226 if (fi.get_type() == tools::fs::file_info::dir_type)
227 errcode = run_test_directory(tp, w);
228 else {
229 const vars_map effective_config =
230 tools::config_file::merge_configs(config, m_cmdline_vars);
231
232 errcode = run_test_program(tp, tc, w, effective_config);
233 }
234 return errcode;
235 }
236
237 int
238 atf_run::run_test_directory(const tools::fs::path& tp,
239 tools::test_program::atf_tps_writer& w)
240 {
241 tools::atffile af = tools::read_atffile(tp / "Atffile");
242
243 vars_map test_suite_vars;
244 {
245 vars_map::const_iterator iter = af.props().find("test-suite");
246 assert(iter != af.props().end());
247 test_suite_vars = tools::config_file::read_config_files((*iter).second);
248 }
249
250 bool ok = true;
251 for (std::vector< std::string >::const_iterator iter = af.tps().begin();
252 iter != af.tps().end(); iter++) {
253 const bool result = run_test(tp / *iter, "", w,
254 tools::config_file::merge_configs(af.conf(), test_suite_vars));
255 ok &= (result == EXIT_SUCCESS);
256 }
257
258 return ok ? EXIT_SUCCESS : EXIT_FAILURE;
259 }
260
261 tools::test_program::test_case_result
262 atf_run::get_test_case_result(const std::string& broken_reason,
263 const tools::process::status& s,
264 const tools::fs::path& resfile)
265 const
266 {
267 using tools::text::to_string;
268 using tools::test_program::read_test_case_result;
269 using tools::test_program::test_case_result;
270
271 if (!broken_reason.empty()) {
272 test_case_result tcr;
273
274 try {
275 tcr = read_test_case_result(resfile);
276
277 if (tcr.state() == "expected_timeout") {
278 return tcr;
279 } else {
280 return test_case_result("failed", -1, broken_reason);
281 }
282 } catch (const std::runtime_error&) {
283 return test_case_result("failed", -1, broken_reason);
284 }
285 }
286
287 if (s.exited()) {
288 test_case_result tcr;
289
290 try {
291 tcr = read_test_case_result(resfile);
292 } catch (const std::runtime_error& e) {
293 return test_case_result("failed", -1, "Test case exited "
294 "normally but failed to create the results file: " +
295 std::string(e.what()));
296 }
297
298 if (tcr.state() == "expected_death") {
299 return tcr;
300 } else if (tcr.state() == "expected_exit") {
301 if (tcr.value() == -1 || s.exitstatus() == tcr.value())
302 return tcr;
303 else
304 return test_case_result("failed", -1, "Test case was "
305 "expected to exit with a " + to_string(tcr.value()) +
306 " error code but returned " + to_string(s.exitstatus()));
307 } else if (tcr.state() == "expected_failure") {
308 if (s.exitstatus() == EXIT_SUCCESS)
309 return tcr;
310 else
311 return test_case_result("failed", -1, "Test case returned an "
312 "error in expected_failure mode but it should not have");
313 } else if (tcr.state() == "expected_signal") {
314 return test_case_result("failed", -1, "Test case exited cleanly "
315 "but was expected to receive a signal");
316 } else if (tcr.state() == "failed") {
317 if (s.exitstatus() == EXIT_SUCCESS)
318 return test_case_result("failed", -1, "Test case "
319 "exited successfully but reported failure");
320 else
321 return tcr;
322 } else if (tcr.state() == "passed") {
323 if (s.exitstatus() == EXIT_SUCCESS)
324 return tcr;
325 else
326 return test_case_result("failed", -1, "Test case exited as "
327 "passed but reported an error");
328 } else if (tcr.state() == "skipped") {
329 if (s.exitstatus() == EXIT_SUCCESS)
330 return tcr;
331 else
332 return test_case_result("failed", -1, "Test case exited as "
333 "skipped but reported an error");
334 }
335 } else if (s.signaled()) {
336 test_case_result tcr;
337
338 try {
339 tcr = read_test_case_result(resfile);
340 } catch (const std::runtime_error&) {
341 return test_case_result("failed", -1, "Test program received "
342 "signal " + tools::text::to_string(s.termsig()) +
343 (s.coredump() ? " (core dumped)" : ""));
344 }
345
346 if (tcr.state() == "expected_death") {
347 return tcr;
348 } else if (tcr.state() == "expected_signal") {
349 if (tcr.value() == -1 || s.termsig() == tcr.value())
350 return tcr;
351 else
352 return test_case_result("failed", -1, "Test case was "
353 "expected to exit due to a " + to_string(tcr.value()) +
354 " signal but got " + to_string(s.termsig()));
355 } else {
356 return test_case_result("failed", -1, "Test program received "
357 "signal " + tools::text::to_string(s.termsig()) +
358 (s.coredump() ? " (core dumped)" : "") + " and created a "
359 "bogus results file");
360 }
361 }
362 std::abort();
363 return test_case_result();
364 }
365
366 int
367 atf_run::run_test_program(const tools::fs::path& tp,
368 const std::string tc,
369 tools::test_program::atf_tps_writer& w,
370 const vars_map& config)
371 {
372 int errcode = EXIT_SUCCESS;
373
374 tools::test_program::metadata md;
375 try {
376 md = tools::test_program::get_metadata(tp, config);
377 } catch (const tools::parser::format_error& e) {
378 w.start_tp(tp.str(), 0);
379 w.end_tp("Invalid format for test case list: " + std::string(e.what()));
380 return EXIT_FAILURE;
381 } catch (const tools::parser::parse_errors& e) {
382 const std::string reason = tools::text::join(e, "; ");
383 w.start_tp(tp.str(), 0);
384 w.end_tp("Invalid format for test case list: " + reason);
385 return EXIT_FAILURE;
386 }
387
388 tools::fs::temp_dir resdir(
389 tools::fs::path(tools::config::get("atf_workdir")) / "atf-run.XXXXXX");
390
391 w.start_tp(tp.str(), md.test_cases.size());
392 if (md.test_cases.empty()) {
393 w.end_tp("Bogus test program: reported 0 test cases");
394 errcode = EXIT_FAILURE;
395 } else {
396 for (std::map< std::string, vars_map >::const_iterator iter
397 = md.test_cases.begin(); iter != md.test_cases.end(); iter++) {
398 const std::string& tcname = (*iter).first;
399 const vars_map& tcmd = (*iter).second;
400
401 if (! tc.empty() && tcname != tc)
402 continue;
403
404 w.start_tc(tcname);
405
406 try {
407 const std::string& reqfail = tools::check_requirements(
408 tcmd, config);
409 if (!reqfail.empty()) {
410 w.end_tc("skipped", reqfail);
411 continue;
412 }
413 } catch (const std::runtime_error& e) {
414 w.end_tc("failed", e.what());
415 errcode = EXIT_FAILURE;
416 continue;
417 }
418
419 const std::pair< int, int > user = tools::get_required_user(
420 tcmd, config);
421
422 tools::fs::path resfile = resdir.get_path() / "tcr";
423 assert(!tools::fs::exists(resfile));
424 try {
425 const bool has_cleanup = tools::text::to_bool(
426 (*tcmd.find("has.cleanup")).second);
427
428 tools::fs::temp_dir workdir(tools::fs::path(tools::config::get(
429 "atf_workdir")) / "atf-run.XXXXXX");
430 if (user.first != -1 && user.second != -1) {
431 if (::chown(workdir.get_path().c_str(), user.first,
432 user.second) == -1) {
433 throw tools::system_error("chown(" +
434 workdir.get_path().str() + ")", "chown(2) failed",
435 errno);
436 }
437 resfile = workdir.get_path() / "tcr";
438 }
439
440 std::pair< std::string, const tools::process::status > s =
441 tools::test_program::run_test_case(
442 tp, tcname, "body", tcmd, config,
443 resfile, workdir.get_path(), w);
444 if (s.second.signaled() && s.second.coredump())
445 dump_stacktrace(tp, s.second, workdir.get_path(), w);
446 if (has_cleanup)
447 (void)tools::test_program::run_test_case(
448 tp, tcname, "cleanup", tcmd,
449 config, resfile, workdir.get_path(), w);
450
451 // TODO: Force deletion of workdir.
452
453 tools::test_program::test_case_result tcr =
454 get_test_case_result(s.first, s.second, resfile);
455
456 w.end_tc(tcr.state(), tcr.reason());
457 if (tcr.state() == "failed")
458 errcode = EXIT_FAILURE;
459 } catch (...) {
460 if (tools::fs::exists(resfile))
461 tools::fs::remove(resfile);
462 throw;
463 }
464 if (tools::fs::exists(resfile))
465 tools::fs::remove(resfile);
466
467 }
468 w.end_tp("");
469 }
470
471 return errcode;
472 }
473
474 static void
475 colon_split(const std::string &s, std::string &tp, std::string &tc)
476 {
477 size_t colon_pos = s.rfind(':');
478 if (colon_pos != std::string::npos && colon_pos < s.size() - 1) {
479 tp = s.substr(0, colon_pos);
480 tc = s.substr(colon_pos + 1);
481 } else {
482 tp = s;
483 tc = "";
484 }
485 }
486
487 size_t
488 atf_run::count_tps(std::vector< std::string > tps)
489 const
490 {
491 size_t ntps = 0;
492
493 for (std::vector< std::string >::const_iterator iter = tps.begin();
494 iter != tps.end(); iter++) {
495 std::string tpname, tcname;
496 colon_split(*iter, tpname, tcname);
497 tools::fs::path tp(tpname);
498 tools::fs::file_info fi(tp);
499
500 if (fi.get_type() == tools::fs::file_info::dir_type) {
501 tools::atffile af = tools::read_atffile(tp / "Atffile");
502 std::vector< std::string > aux = af.tps();
503 for (std::vector< std::string >::iterator i2 = aux.begin();
504 i2 != aux.end(); i2++)
505 *i2 = (tp / *i2).str();
506 ntps += count_tps(aux);
507 } else
508 ntps++;
509 }
510
511 return ntps;
512 }
513
514 static
515 void
516 call_hook(const std::string& tool, const std::string& hook)
517 {
518 const tools::fs::path sh(tools::config::get("atf_shell"));
519 const tools::fs::path hooks =
520 tools::fs::path(tools::config::get("atf_pkgdatadir")) / (tool + ".hooks");
521
522 const tools::process::status s =
523 tools::process::exec(sh,
524 tools::process::argv_array(sh.c_str(), hooks.c_str(),
525 hook.c_str(), NULL),
526 tools::process::stream_inherit(),
527 tools::process::stream_inherit());
528
529
530 if (!s.exited() || s.exitstatus() != EXIT_SUCCESS)
531 throw std::runtime_error("Failed to run the '" + hook + "' hook "
532 "for '" + tool + "'");
533 }
534
535 int
536 atf_run::main(void)
537 {
538 tools::atffile af = tools::read_atffile(tools::fs::path("Atffile"));
539
540 std::vector< std::string > tps;
541 tps = af.tps();
542 if (m_argc >= 1) {
543 // TODO: Ensure that the given test names are listed in the
544 // Atffile. Take into account that the file can be using globs.
545 tps.clear();
546 for (int i = 0; i < m_argc; i++)
547 tps.push_back(m_argv[i]);
548 }
549
550 // Read configuration data for this test suite.
551 vars_map test_suite_vars;
552 {
553 vars_map::const_iterator iter = af.props().find("test-suite");
554 assert(iter != af.props().end());
555 test_suite_vars = tools::config_file::read_config_files((*iter).second);
556 }
557
558 tools::test_program::atf_tps_writer w(std::cout);
559 call_hook("atf-run", "info_start_hook");
560 w.ntps(count_tps(tps));
561
562 bool ok = true;
563 for (std::vector< std::string >::const_iterator iter = tps.begin();
564 iter != tps.end(); iter++) {
565 std::string tp, tc;
566 colon_split(*iter, tp, tc);
567 const bool result = run_test(tools::fs::path(tp), tc, w,
568 tools::config_file::merge_configs(af.conf(), test_suite_vars));
569 ok &= (result == EXIT_SUCCESS);
570 }
571
572 call_hook("atf-run", "info_end_hook");
573
574 return ok ? EXIT_SUCCESS : EXIT_FAILURE;
575 }
576
577 int
578 main(int argc, char* const* argv)
579 {
580 return atf_run().run(argc, argv);
581 }
582