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/stat.h>
33 #include <sys/time.h>
34 
35 #include <fcntl.h>
36 #include <signal.h>
37 #include <unistd.h>
38 }
39 
40 #include <cassert>
41 #include <cerrno>
42 #include <cstdlib>
43 #include <cstring>
44 #include <fstream>
45 #include <iostream>
46 
47 #include "config_file.hpp"
48 #include "env.hpp"
49 #include "fs.hpp"
50 #include "io.hpp"
51 #include "parser.hpp"
52 #include "process.hpp"
53 #include "requirements.hpp"
54 #include "signals.hpp"
55 #include "test-program.hpp"
56 #include "text.hpp"
57 #include "timers.hpp"
58 #include "user.hpp"
59 
60 namespace impl = tools::test_program;
61 namespace detail = tools::test_program::detail;
62 
63 namespace {
64 
65 typedef std::map< std::string, std::string > vars_map;
66 
67 static void
68 check_stream(std::ostream& os)
69 {
70     // If we receive a signal while writing to the stream, the bad bit gets set.
71     // Things seem to behave fine afterwards if we clear such error condition.
72     // However, I'm not sure if it's safe to query errno at this point.
73     if (os.bad()) {
74         if (errno == EINTR)
75             os.clear();
76         else
77             throw std::runtime_error("Failed");
78     }
79 }
80 
81 namespace atf_tp {
82 
83 static const tools::parser::token_type eof_type = 0;
84 static const tools::parser::token_type nl_type = 1;
85 static const tools::parser::token_type text_type = 2;
86 static const tools::parser::token_type colon_type = 3;
87 static const tools::parser::token_type dblquote_type = 4;
88 
89 class tokenizer : public tools::parser::tokenizer< std::istream > {
90 public:
91     tokenizer(std::istream& is, size_t curline) :
92         tools::parser::tokenizer< std::istream >
93             (is, true, eof_type, nl_type, text_type, curline)
94     {
95         add_delim(':', colon_type);
96         add_quote('"', dblquote_type);
97     }
98 };
99 
100 } // namespace atf_tp
101 
102 class metadata_reader : public detail::atf_tp_reader {
103     impl::test_cases_map m_tcs;
104 
105     void got_tc(const std::string& ident, const vars_map& props)
106     {
107         if (m_tcs.find(ident) != m_tcs.end())
108             throw(std::runtime_error("Duplicate test case " + ident +
109                                      " in test program"));
110         m_tcs[ident] = props;
111 
112         if (m_tcs[ident].find("has.cleanup") == m_tcs[ident].end())
113             m_tcs[ident].insert(std::make_pair("has.cleanup", "false"));
114 
115         if (m_tcs[ident].find("timeout") == m_tcs[ident].end())
116             m_tcs[ident].insert(std::make_pair("timeout", "300"));
117     }
118 
119 public:
120     metadata_reader(std::istream& is) :
121         detail::atf_tp_reader(is)
122     {
123     }
124 
125     const impl::test_cases_map&
126     get_tcs(void)
127         const
128     {
129         return m_tcs;
130     }
131 };
132 
133 struct get_metadata_params {
134     const tools::fs::path& executable;
135     const vars_map& config;
136 
137     get_metadata_params(const tools::fs::path& p_executable,
138                         const vars_map& p_config) :
139         executable(p_executable),
140         config(p_config)
141     {
142     }
143 };
144 
145 struct test_case_params {
146     const tools::fs::path& executable;
147     const std::string& test_case_name;
148     const std::string& test_case_part;
149     const vars_map& metadata;
150     const vars_map& config;
151     const tools::fs::path& resfile;
152     const tools::fs::path& workdir;
153 
154     test_case_params(const tools::fs::path& p_executable,
155                      const std::string& p_test_case_name,
156                      const std::string& p_test_case_part,
157                      const vars_map& p_metadata,
158                      const vars_map& p_config,
159                      const tools::fs::path& p_resfile,
160                      const tools::fs::path& p_workdir) :
161         executable(p_executable),
162         test_case_name(p_test_case_name),
163         test_case_part(p_test_case_part),
164         metadata(p_metadata),
165         config(p_config),
166         resfile(p_resfile),
167         workdir(p_workdir)
168     {
169     }
170 };
171 
172 static
173 std::string
174 generate_timestamp(void)
175 {
176     struct timeval tv;
177     if (gettimeofday(&tv, NULL) == -1)
178         return "0.0";
179 
180     char buf[32];
181     const int len = snprintf(buf, sizeof(buf), "%ld.%ld",
182                              static_cast< long >(tv.tv_sec),
183                              static_cast< long >(tv.tv_usec));
184     if (len >= static_cast< int >(sizeof(buf)) || len < 0)
185         return "0.0";
186     else
187         return buf;
188 }
189 
190 static
191 void
192 append_to_vector(std::vector< std::string >& v1,
193                  const std::vector< std::string >& v2)
194 {
195     std::copy(v2.begin(), v2.end(),
196               std::back_insert_iterator< std::vector< std::string > >(v1));
197 }
198 
199 static
200 char**
201 vector_to_argv(const std::vector< std::string >& v)
202 {
203     char** argv = new char*[v.size() + 1];
204     for (std::vector< std::string >::size_type i = 0; i < v.size(); i++) {
205         argv[i] = strdup(v[i].c_str());
206     }
207     argv[v.size()] = NULL;
208     return argv;
209 }
210 
211 static
212 void
213 exec_or_exit(const tools::fs::path& executable,
214              const std::vector< std::string >& argv)
215 {
216     // This leaks memory in case of a failure, but it is OK.  Exiting will
217     // do the necessary cleanup.
218     char* const* native_argv = vector_to_argv(argv);
219 
220     ::execv(executable.c_str(), native_argv);
221 
222     const std::string message = "Failed to execute '" + executable.str() +
223         "': " + std::strerror(errno) + "\n";
224     if (::write(STDERR_FILENO, message.c_str(), message.length()) == -1)
225         std::abort();
226     std::exit(EXIT_FAILURE);
227 }
228 
229 static
230 std::vector< std::string >
231 config_to_args(const vars_map& config)
232 {
233     std::vector< std::string > args;
234 
235     for (vars_map::const_iterator iter = config.begin();
236          iter != config.end(); iter++)
237         args.push_back("-v" + (*iter).first + "=" + (*iter).second);
238 
239     return args;
240 }
241 
242 static
243 void
244 silence_stdin(void)
245 {
246     ::close(STDIN_FILENO);
247     int fd = ::open("/dev/null", O_RDONLY);
248     if (fd == -1)
249         throw std::runtime_error("Could not open /dev/null");
250     assert(fd == STDIN_FILENO);
251 }
252 
253 static
254 void
255 prepare_child(const tools::fs::path& workdir)
256 {
257     const int ret = ::setpgid(::getpid(), 0);
258     assert(ret != -1);
259 
260     ::umask(S_IWGRP | S_IWOTH);
261 
262     for (int i = 1; i <= tools::signals::last_signo; i++)
263         tools::signals::reset(i);
264 
265     tools::env::set("HOME", workdir.str());
266     tools::env::unset("LANG");
267     tools::env::unset("LC_ALL");
268     tools::env::unset("LC_COLLATE");
269     tools::env::unset("LC_CTYPE");
270     tools::env::unset("LC_MESSAGES");
271     tools::env::unset("LC_MONETARY");
272     tools::env::unset("LC_NUMERIC");
273     tools::env::unset("LC_TIME");
274     tools::env::set("TZ", "UTC");
275 
276     tools::env::set("__RUNNING_INSIDE_ATF_RUN", "internal-yes-value");
277 
278     tools::fs::change_directory(workdir);
279 
280     silence_stdin();
281 }
282 
283 static
284 void
285 get_metadata_child(void* raw_params)
286 {
287     const get_metadata_params* params =
288         static_cast< const get_metadata_params* >(raw_params);
289 
290     std::vector< std::string > argv;
291     argv.push_back(params->executable.leaf_name());
292     argv.push_back("-l");
293     argv.push_back("-s" + params->executable.branch_path().str());
294     append_to_vector(argv, config_to_args(params->config));
295 
296     exec_or_exit(params->executable, argv);
297 }
298 
299 void
300 run_test_case_child(void* raw_params)
301 {
302     const test_case_params* params =
303         static_cast< const test_case_params* >(raw_params);
304 
305     const std::pair< int, int > user = tools::get_required_user(
306         params->metadata, params->config);
307     if (user.first != -1 && user.second != -1)
308         tools::user::drop_privileges(user);
309 
310     // The input 'tp' parameter may be relative and become invalid once
311     // we change the current working directory.
312     const tools::fs::path absolute_executable = params->executable.to_absolute();
313 
314     // Prepare the test program's arguments.  We use dynamic memory and
315     // do not care to release it.  We are going to die anyway very soon,
316     // either due to exec(2) or to exit(3).
317     std::vector< std::string > argv;
318     argv.push_back(absolute_executable.leaf_name());
319     argv.push_back("-r" + params->resfile.str());
320     argv.push_back("-s" + absolute_executable.branch_path().str());
321     append_to_vector(argv, config_to_args(params->config));
322     argv.push_back(params->test_case_name + ":" + params->test_case_part);
323 
324     prepare_child(params->workdir);
325     exec_or_exit(absolute_executable, argv);
326 }
327 
328 static void
329 tokenize_result(const std::string& line, std::string& out_state,
330                 std::string& out_arg, std::string& out_reason)
331 {
332     const std::string::size_type pos = line.find_first_of(":(");
333     if (pos == std::string::npos) {
334         out_state = line;
335         out_arg = "";
336         out_reason = "";
337     } else if (line[pos] == ':') {
338         out_state = line.substr(0, pos);
339         out_arg = "";
340         out_reason = tools::text::trim(line.substr(pos + 1));
341     } else if (line[pos] == '(') {
342         const std::string::size_type pos2 = line.find("):", pos);
343         if (pos2 == std::string::npos)
344             throw std::runtime_error("Invalid test case result '" + line +
345                 "': unclosed optional argument");
346         out_state = line.substr(0, pos);
347         out_arg = line.substr(pos + 1, pos2 - pos - 1);
348         out_reason = tools::text::trim(line.substr(pos2 + 2));
349     } else
350         std::abort();
351 }
352 
353 static impl::test_case_result
354 handle_result(const std::string& state, const std::string& arg,
355               const std::string& reason)
356 {
357     assert(state == "passed");
358 
359     if (!arg.empty() || !reason.empty())
360         throw std::runtime_error("The test case result '" + state + "' cannot "
361             "be accompanied by a reason nor an expected value");
362 
363     return impl::test_case_result(state, -1, reason);
364 }
365 
366 static impl::test_case_result
367 handle_result_with_reason(const std::string& state, const std::string& arg,
368                           const std::string& reason)
369 {
370     assert(state == "expected_death" || state == "expected_failure" ||
371         state == "expected_timeout" || state == "failed" || state == "skipped");
372 
373     if (!arg.empty() || reason.empty())
374         throw std::runtime_error("The test case result '" + state + "' must "
375             "be accompanied by a reason but not by an expected value");
376 
377     return impl::test_case_result(state, -1, reason);
378 }
379 
380 static impl::test_case_result
381 handle_result_with_reason_and_arg(const std::string& state,
382                                   const std::string& arg,
383                                   const std::string& reason)
384 {
385     assert(state == "expected_exit" || state == "expected_signal");
386 
387     if (reason.empty())
388         throw std::runtime_error("The test case result '" + state + "' must "
389             "be accompanied by a reason");
390 
391     int value;
392     if (arg.empty()) {
393         value = -1;
394     } else {
395         try {
396             value = tools::text::to_type< int >(arg);
397         } catch (const std::runtime_error&) {
398             throw std::runtime_error("The value '" + arg + "' passed to the '" +
399                 state + "' state must be an integer");
400         }
401     }
402 
403     return impl::test_case_result(state, value, reason);
404 }
405 
406 } // anonymous namespace
407 
408 detail::atf_tp_reader::atf_tp_reader(std::istream& is) :
409     m_is(is)
410 {
411 }
412 
413 detail::atf_tp_reader::~atf_tp_reader(void)
414 {
415 }
416 
417 void
418 detail::atf_tp_reader::got_tc(
419     const std::string& ident __attribute__((__unused__)),
420     const std::map< std::string, std::string >& md __attribute__((__unused__)))
421 {
422 }
423 
424 void
425 detail::atf_tp_reader::got_eof(void)
426 {
427 }
428 
429 void
430 detail::atf_tp_reader::validate_and_insert(const std::string& name,
431     const std::string& value, const size_t lineno,
432     std::map< std::string, std::string >& md)
433 {
434     using tools::parser::parse_error;
435 
436     if (value.empty())
437         throw parse_error(lineno, "The value for '" + name +"' cannot be "
438                           "empty");
439 
440     const std::string ident_regex = "^[_A-Za-z0-9]+$";
441     const std::string integer_regex = "^[0-9]+$";
442 
443     if (name == "descr") {
444         // Any non-empty value is valid.
445     } else if (name == "has.cleanup") {
446         try {
447             (void)tools::text::to_bool(value);
448         } catch (const std::runtime_error&) {
449             throw parse_error(lineno, "The has.cleanup property requires a"
450                               " boolean value");
451         }
452     } else if (name == "ident") {
453         if (!tools::text::match(value, ident_regex))
454             throw parse_error(lineno, "The identifier must match " +
455                               ident_regex + "; was '" + value + "'");
456     } else if (name == "require.arch") {
457     } else if (name == "require.config") {
458     } else if (name == "require.files") {
459     } else if (name == "require.machine") {
460     } else if (name == "require.memory") {
461         try {
462             (void)tools::text::to_bytes(value);
463         } catch (const std::runtime_error&) {
464             throw parse_error(lineno, "The require.memory property requires an "
465                               "integer value representing an amount of bytes");
466         }
467     } else if (name == "require.progs") {
468     } else if (name == "require.user") {
469     } else if (name == "timeout") {
470         if (!tools::text::match(value, integer_regex))
471             throw parse_error(lineno, "The timeout property requires an integer"
472                               " value");
473     } else if (name == "use.fs") {
474         // Deprecated; ignore it.
475     } else if (name.size() > 2 && name[0] == 'X' && name[1] == '-') {
476         // Any non-empty value is valid.
477     } else {
478         throw parse_error(lineno, "Unknown property '" + name + "'");
479     }
480 
481     md.insert(std::make_pair(name, value));
482 }
483 
484 void
485 detail::atf_tp_reader::read(void)
486 {
487     using tools::parser::parse_error;
488     using namespace atf_tp;
489 
490     std::pair< size_t, tools::parser::headers_map > hml =
491         tools::parser::read_headers(m_is, 1);
492     tools::parser::validate_content_type(hml.second,
493         "application/X-atf-tp", 1);
494 
495     tokenizer tkz(m_is, hml.first);
496     tools::parser::parser< tokenizer > p(tkz);
497 
498     try {
499         tools::parser::token t = p.expect(text_type, "property name");
500         if (t.text() != "ident")
501             throw parse_error(t.lineno(), "First property of a test case "
502                               "must be 'ident'");
503 
504         std::map< std::string, std::string > props;
505         do {
506             const std::string name = t.text();
507             t = p.expect(colon_type, "`:'");
508             const std::string value = tools::text::trim(p.rest_of_line());
509             t = p.expect(nl_type, "new line");
510             validate_and_insert(name, value, t.lineno(), props);
511 
512             t = p.expect(eof_type, nl_type, text_type, "property name, new "
513                          "line or eof");
514             if (t.type() == nl_type || t.type() == eof_type) {
515                 const std::map< std::string, std::string >::const_iterator
516                     iter = props.find("ident");
517                 if (iter == props.end())
518                     throw parse_error(t.lineno(), "Test case definition did "
519                                       "not define an 'ident' property");
520                 ATF_PARSER_CALLBACK(p, got_tc((*iter).second, props));
521                 props.clear();
522 
523                 if (t.type() == nl_type) {
524                     t = p.expect(text_type, "property name");
525                     if (t.text() != "ident")
526                         throw parse_error(t.lineno(), "First property of a "
527                                           "test case must be 'ident'");
528                 }
529             }
530         } while (t.type() != eof_type);
531         ATF_PARSER_CALLBACK(p, got_eof());
532     } catch (const parse_error& pe) {
533         p.add_error(pe);
534         p.reset(nl_type);
535     }
536 }
537 
538 impl::test_case_result
539 detail::parse_test_case_result(const std::string& line)
540 {
541     std::string state, arg, reason;
542     tokenize_result(line, state, arg, reason);
543 
544     if (state == "expected_death")
545         return handle_result_with_reason(state, arg, reason);
546     else if (state.compare(0, 13, "expected_exit") == 0)
547         return handle_result_with_reason_and_arg(state, arg, reason);
548     else if (state.compare(0, 16, "expected_failure") == 0)
549         return handle_result_with_reason(state, arg, reason);
550     else if (state.compare(0, 15, "expected_signal") == 0)
551         return handle_result_with_reason_and_arg(state, arg, reason);
552     else if (state.compare(0, 16, "expected_timeout") == 0)
553         return handle_result_with_reason(state, arg, reason);
554     else if (state == "failed")
555         return handle_result_with_reason(state, arg, reason);
556     else if (state == "passed")
557         return handle_result(state, arg, reason);
558     else if (state == "skipped")
559         return handle_result_with_reason(state, arg, reason);
560     else
561         throw std::runtime_error("Unknown test case result type in: " + line);
562 }
563 
564 impl::atf_tps_writer::atf_tps_writer(std::ostream& os) :
565     m_os(os)
566 {
567     tools::parser::headers_map hm;
568     tools::parser::attrs_map ct_attrs;
569     ct_attrs["version"] = "3";
570     hm["Content-Type"] =
571         tools::parser::header_entry("Content-Type", "application/X-atf-tps",
572                                   ct_attrs);
573     tools::parser::write_headers(hm, m_os);
574 }
575 
576 void
577 impl::atf_tps_writer::info(const std::string& what, const std::string& val)
578 {
579     m_os << "info: " << what << ", " << val << "\n";
580     m_os.flush();
581 }
582 
583 void
584 impl::atf_tps_writer::ntps(size_t p_ntps)
585 {
586     m_os << "tps-count: " << p_ntps << "\n";
587     m_os.flush();
588 }
589 
590 void
591 impl::atf_tps_writer::start_tp(const std::string& tp, size_t ntcs)
592 {
593     m_tpname = tp;
594     m_os << "tp-start: " << generate_timestamp() << ", " << tp << ", "
595          << ntcs << "\n";
596     m_os.flush();
597 }
598 
599 void
600 impl::atf_tps_writer::end_tp(const std::string& reason)
601 {
602     assert(reason.find('\n') == std::string::npos);
603     if (reason.empty())
604         m_os << "tp-end: " << generate_timestamp() << ", " << m_tpname << "\n";
605     else
606         m_os << "tp-end: " << generate_timestamp() << ", " << m_tpname
607              << ", " << reason << "\n";
608     m_os.flush();
609 }
610 
611 void
612 impl::atf_tps_writer::start_tc(const std::string& tcname)
613 {
614     m_tcname = tcname;
615     m_os << "tc-start: " << generate_timestamp() << ", " << tcname << "\n";
616     m_os.flush();
617 }
618 
619 void
620 impl::atf_tps_writer::stdout_tc(const std::string& line)
621 {
622     m_os << "tc-so:" << line << "\n";
623     check_stream(m_os);
624     m_os.flush();
625     check_stream(m_os);
626 }
627 
628 void
629 impl::atf_tps_writer::stderr_tc(const std::string& line)
630 {
631     m_os << "tc-se:" << line << "\n";
632     check_stream(m_os);
633     m_os.flush();
634     check_stream(m_os);
635 }
636 
637 void
638 impl::atf_tps_writer::end_tc(const std::string& state,
639                              const std::string& reason)
640 {
641     std::string str =  ", " + m_tcname + ", " + state;
642     if (!reason.empty())
643         str += ", " + reason;
644     m_os << "tc-end: " << generate_timestamp() << str << "\n";
645     m_os.flush();
646 }
647 
648 impl::metadata
649 impl::get_metadata(const tools::fs::path& executable,
650                    const vars_map& config)
651 {
652     get_metadata_params params(executable, config);
653     tools::process::child child =
654         tools::process::fork(get_metadata_child,
655                            tools::process::stream_capture(),
656                            tools::process::stream_inherit(),
657                            static_cast< void * >(&params));
658 
659     tools::io::pistream outin(child.stdout_fd());
660 
661     metadata_reader parser(outin);
662     parser.read();
663 
664     const tools::process::status status = child.wait();
665     if (!status.exited() || status.exitstatus() != EXIT_SUCCESS)
666         throw tools::parser::format_error("Test program returned failure "
667                                         "exit status for test case list");
668 
669     return metadata(parser.get_tcs());
670 }
671 
672 impl::test_case_result
673 impl::read_test_case_result(const tools::fs::path& results_path)
674 {
675     std::ifstream results_file(results_path.c_str());
676     if (!results_file)
677         throw std::runtime_error("Failed to open " + results_path.str());
678 
679     std::string line, extra_line;
680     std::getline(results_file, line);
681     if (!results_file.good())
682         throw std::runtime_error("Results file is empty");
683 
684     while (std::getline(results_file, extra_line).good())
685         line += "<<NEWLINE UNEXPECTED>>" + extra_line;
686 
687     results_file.close();
688 
689     return detail::parse_test_case_result(line);
690 }
691 
692 namespace {
693 
694 static volatile bool terminate_poll;
695 
696 static void
697 sigchld_handler(const int signo __attribute__((__unused__)))
698 {
699     terminate_poll = true;
700 }
701 
702 class child_muxer : public tools::io::muxer {
703     impl::atf_tps_writer& m_writer;
704 
705     void
706     line_callback(const size_t index, const std::string& line)
707     {
708         switch (index) {
709         case 0: m_writer.stdout_tc(line); break;
710         case 1: m_writer.stderr_tc(line); break;
711         default: std::abort();
712         }
713     }
714 
715 public:
716     child_muxer(const int* fds, const size_t nfds,
717                 impl::atf_tps_writer& writer) :
718         muxer(fds, nfds),
719         m_writer(writer)
720     {
721     }
722 };
723 
724 } // anonymous namespace
725 
726 std::pair< std::string, tools::process::status >
727 impl::run_test_case(const tools::fs::path& executable,
728                     const std::string& test_case_name,
729                     const std::string& test_case_part,
730                     const vars_map& metadata,
731                     const vars_map& config,
732                     const tools::fs::path& resfile,
733                     const tools::fs::path& workdir,
734                     atf_tps_writer& writer)
735 {
736     // TODO: Capture termination signals and deliver them to the subprocess
737     // instead.  Or maybe do something else; think about it.
738 
739     test_case_params params(executable, test_case_name, test_case_part,
740                             metadata, config, resfile, workdir);
741     tools::process::child child =
742         tools::process::fork(run_test_case_child,
743                            tools::process::stream_capture(),
744                            tools::process::stream_capture(),
745                            static_cast< void * >(&params));
746 
747     terminate_poll = false;
748 
749     const vars_map::const_iterator iter = metadata.find("timeout");
750     assert(iter != metadata.end());
751     const unsigned int timeout =
752         tools::text::to_type< unsigned int >((*iter).second);
753     const pid_t child_pid = child.pid();
754 
755     // Get the input stream of stdout and stderr.
756     tools::io::file_handle outfh = child.stdout_fd();
757     tools::io::file_handle errfh = child.stderr_fd();
758 
759     bool timed_out = false;
760 
761     // Process the test case's output and multiplex it into our output
762     // stream as we read it.
763     int fds[2] = {outfh.get(), errfh.get()};
764     child_muxer mux(fds, 2, writer);
765     try {
766         timers::child_timer timeout_timer(timeout, child_pid, terminate_poll);
767         signals::signal_programmer sigchld(SIGCHLD, sigchld_handler);
768         mux.mux(terminate_poll);
769         timed_out = timeout_timer.fired();
770     } catch (...) {
771         std::abort();
772     }
773 
774     ::killpg(child_pid, SIGKILL);
775     mux.flush();
776     tools::process::status status = child.wait();
777 
778     std::string reason;
779 
780     if (timed_out) {
781         // Don't assume the child process has been signaled due to the timeout
782         // expiration as older versions did.  The child process may have exited
783         // but we may have timed out due to a subchild process getting stuck.
784         reason = "Test case timed out after " + tools::text::to_string(timeout) +
785             " " + (timeout == 1 ? "second" : "seconds");
786     }
787 
788     return std::make_pair(reason, status);
789 }
790