8fee91db34
Experimental version released on February 7th, 2014. This is the last release to bundle the code for the deprecated tools. The next release will drop their code and will stop worrying about backwards compatibility between the ATF libraries and what the old tools may or may not support. If you still require the old tools for some reason, grab a copy of the 'tools' directory now. The code in this directory is standalone and does not depend on any internal details of atf-c++ any longer. * Various fixes and improvements to support running as part of the FreeBSD test suite. * Project hosting moved from Google Code (as a subproject of Kyua) to GitHub (as a first-class project). The main reason for the change is the suppression of binary downloads in Google Code on Jan 15th, 2014. See https://github.com/jmmv/atf/ * Removed builtin help from atf-sh(1) and atf-check(1) for simplicity reasons. In other words, their -h option is gone. * Moved the code of the deprecated tools into a 'tools' directory and completely decoupled their code from the internals of atf-c++. The reason for this is to painlessly allow a third-party to maintain a copy of these tools after we delete them because upcoming changes to atf-c++ would break the stale tools.
791 lines
24 KiB
C++
791 lines
24 KiB
C++
//
|
|
// Automated Testing Framework (atf)
|
|
//
|
|
// Copyright (c) 2007 The NetBSD Foundation, Inc.
|
|
// All rights reserved.
|
|
//
|
|
// Redistribution and use in source and binary forms, with or without
|
|
// modification, are permitted provided that the following conditions
|
|
// are met:
|
|
// 1. Redistributions of source code must retain the above copyright
|
|
// notice, this list of conditions and the following disclaimer.
|
|
// 2. Redistributions in binary form must reproduce the above copyright
|
|
// notice, this list of conditions and the following disclaimer in the
|
|
// documentation and/or other materials provided with the distribution.
|
|
//
|
|
// THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND
|
|
// CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
|
// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
|
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
|
// IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS BE LIABLE FOR ANY
|
|
// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
|
|
// GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
|
|
// IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
|
|
// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
|
|
// IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
//
|
|
|
|
extern "C" {
|
|
#include <sys/types.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/time.h>
|
|
|
|
#include <fcntl.h>
|
|
#include <signal.h>
|
|
#include <unistd.h>
|
|
}
|
|
|
|
#include <cassert>
|
|
#include <cerrno>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <fstream>
|
|
#include <iostream>
|
|
|
|
#include "config_file.hpp"
|
|
#include "defs.hpp"
|
|
#include "env.hpp"
|
|
#include "fs.hpp"
|
|
#include "io.hpp"
|
|
#include "parser.hpp"
|
|
#include "process.hpp"
|
|
#include "requirements.hpp"
|
|
#include "signals.hpp"
|
|
#include "test-program.hpp"
|
|
#include "text.hpp"
|
|
#include "timers.hpp"
|
|
#include "user.hpp"
|
|
|
|
namespace impl = tools::test_program;
|
|
namespace detail = tools::test_program::detail;
|
|
|
|
namespace {
|
|
|
|
typedef std::map< std::string, std::string > vars_map;
|
|
|
|
static void
|
|
check_stream(std::ostream& os)
|
|
{
|
|
// If we receive a signal while writing to the stream, the bad bit gets set.
|
|
// Things seem to behave fine afterwards if we clear such error condition.
|
|
// However, I'm not sure if it's safe to query errno at this point.
|
|
if (os.bad()) {
|
|
if (errno == EINTR)
|
|
os.clear();
|
|
else
|
|
throw std::runtime_error("Failed");
|
|
}
|
|
}
|
|
|
|
namespace atf_tp {
|
|
|
|
static const tools::parser::token_type eof_type = 0;
|
|
static const tools::parser::token_type nl_type = 1;
|
|
static const tools::parser::token_type text_type = 2;
|
|
static const tools::parser::token_type colon_type = 3;
|
|
static const tools::parser::token_type dblquote_type = 4;
|
|
|
|
class tokenizer : public tools::parser::tokenizer< std::istream > {
|
|
public:
|
|
tokenizer(std::istream& is, size_t curline) :
|
|
tools::parser::tokenizer< std::istream >
|
|
(is, true, eof_type, nl_type, text_type, curline)
|
|
{
|
|
add_delim(':', colon_type);
|
|
add_quote('"', dblquote_type);
|
|
}
|
|
};
|
|
|
|
} // namespace atf_tp
|
|
|
|
class metadata_reader : public detail::atf_tp_reader {
|
|
impl::test_cases_map m_tcs;
|
|
|
|
void got_tc(const std::string& ident, const vars_map& props)
|
|
{
|
|
if (m_tcs.find(ident) != m_tcs.end())
|
|
throw(std::runtime_error("Duplicate test case " + ident +
|
|
" in test program"));
|
|
m_tcs[ident] = props;
|
|
|
|
if (m_tcs[ident].find("has.cleanup") == m_tcs[ident].end())
|
|
m_tcs[ident].insert(std::make_pair("has.cleanup", "false"));
|
|
|
|
if (m_tcs[ident].find("timeout") == m_tcs[ident].end())
|
|
m_tcs[ident].insert(std::make_pair("timeout", "300"));
|
|
}
|
|
|
|
public:
|
|
metadata_reader(std::istream& is) :
|
|
detail::atf_tp_reader(is)
|
|
{
|
|
}
|
|
|
|
const impl::test_cases_map&
|
|
get_tcs(void)
|
|
const
|
|
{
|
|
return m_tcs;
|
|
}
|
|
};
|
|
|
|
struct get_metadata_params {
|
|
const tools::fs::path& executable;
|
|
const vars_map& config;
|
|
|
|
get_metadata_params(const tools::fs::path& p_executable,
|
|
const vars_map& p_config) :
|
|
executable(p_executable),
|
|
config(p_config)
|
|
{
|
|
}
|
|
};
|
|
|
|
struct test_case_params {
|
|
const tools::fs::path& executable;
|
|
const std::string& test_case_name;
|
|
const std::string& test_case_part;
|
|
const vars_map& metadata;
|
|
const vars_map& config;
|
|
const tools::fs::path& resfile;
|
|
const tools::fs::path& workdir;
|
|
|
|
test_case_params(const tools::fs::path& p_executable,
|
|
const std::string& p_test_case_name,
|
|
const std::string& p_test_case_part,
|
|
const vars_map& p_metadata,
|
|
const vars_map& p_config,
|
|
const tools::fs::path& p_resfile,
|
|
const tools::fs::path& p_workdir) :
|
|
executable(p_executable),
|
|
test_case_name(p_test_case_name),
|
|
test_case_part(p_test_case_part),
|
|
metadata(p_metadata),
|
|
config(p_config),
|
|
resfile(p_resfile),
|
|
workdir(p_workdir)
|
|
{
|
|
}
|
|
};
|
|
|
|
static
|
|
std::string
|
|
generate_timestamp(void)
|
|
{
|
|
struct timeval tv;
|
|
if (gettimeofday(&tv, NULL) == -1)
|
|
return "0.0";
|
|
|
|
char buf[32];
|
|
const int len = snprintf(buf, sizeof(buf), "%ld.%ld",
|
|
static_cast< long >(tv.tv_sec),
|
|
static_cast< long >(tv.tv_usec));
|
|
if (len >= static_cast< int >(sizeof(buf)) || len < 0)
|
|
return "0.0";
|
|
else
|
|
return buf;
|
|
}
|
|
|
|
static
|
|
void
|
|
append_to_vector(std::vector< std::string >& v1,
|
|
const std::vector< std::string >& v2)
|
|
{
|
|
std::copy(v2.begin(), v2.end(),
|
|
std::back_insert_iterator< std::vector< std::string > >(v1));
|
|
}
|
|
|
|
static
|
|
char**
|
|
vector_to_argv(const std::vector< std::string >& v)
|
|
{
|
|
char** argv = new char*[v.size() + 1];
|
|
for (std::vector< std::string >::size_type i = 0; i < v.size(); i++) {
|
|
argv[i] = strdup(v[i].c_str());
|
|
}
|
|
argv[v.size()] = NULL;
|
|
return argv;
|
|
}
|
|
|
|
static
|
|
void
|
|
exec_or_exit(const tools::fs::path& executable,
|
|
const std::vector< std::string >& argv)
|
|
{
|
|
// This leaks memory in case of a failure, but it is OK. Exiting will
|
|
// do the necessary cleanup.
|
|
char* const* native_argv = vector_to_argv(argv);
|
|
|
|
::execv(executable.c_str(), native_argv);
|
|
|
|
const std::string message = "Failed to execute '" + executable.str() +
|
|
"': " + std::strerror(errno) + "\n";
|
|
if (::write(STDERR_FILENO, message.c_str(), message.length()) == -1)
|
|
std::abort();
|
|
std::exit(EXIT_FAILURE);
|
|
}
|
|
|
|
static
|
|
std::vector< std::string >
|
|
config_to_args(const vars_map& config)
|
|
{
|
|
std::vector< std::string > args;
|
|
|
|
for (vars_map::const_iterator iter = config.begin();
|
|
iter != config.end(); iter++)
|
|
args.push_back("-v" + (*iter).first + "=" + (*iter).second);
|
|
|
|
return args;
|
|
}
|
|
|
|
static
|
|
void
|
|
silence_stdin(void)
|
|
{
|
|
::close(STDIN_FILENO);
|
|
int fd = ::open("/dev/null", O_RDONLY);
|
|
if (fd == -1)
|
|
throw std::runtime_error("Could not open /dev/null");
|
|
assert(fd == STDIN_FILENO);
|
|
}
|
|
|
|
static
|
|
void
|
|
prepare_child(const tools::fs::path& workdir)
|
|
{
|
|
const int ret = ::setpgid(::getpid(), 0);
|
|
assert(ret != -1);
|
|
|
|
::umask(S_IWGRP | S_IWOTH);
|
|
|
|
for (int i = 1; i <= tools::signals::last_signo; i++)
|
|
tools::signals::reset(i);
|
|
|
|
tools::env::set("HOME", workdir.str());
|
|
tools::env::unset("LANG");
|
|
tools::env::unset("LC_ALL");
|
|
tools::env::unset("LC_COLLATE");
|
|
tools::env::unset("LC_CTYPE");
|
|
tools::env::unset("LC_MESSAGES");
|
|
tools::env::unset("LC_MONETARY");
|
|
tools::env::unset("LC_NUMERIC");
|
|
tools::env::unset("LC_TIME");
|
|
tools::env::set("TZ", "UTC");
|
|
|
|
tools::env::set("__RUNNING_INSIDE_ATF_RUN", "internal-yes-value");
|
|
|
|
tools::fs::change_directory(workdir);
|
|
|
|
silence_stdin();
|
|
}
|
|
|
|
static
|
|
void
|
|
get_metadata_child(void* raw_params)
|
|
{
|
|
const get_metadata_params* params =
|
|
static_cast< const get_metadata_params* >(raw_params);
|
|
|
|
std::vector< std::string > argv;
|
|
argv.push_back(params->executable.leaf_name());
|
|
argv.push_back("-l");
|
|
argv.push_back("-s" + params->executable.branch_path().str());
|
|
append_to_vector(argv, config_to_args(params->config));
|
|
|
|
exec_or_exit(params->executable, argv);
|
|
}
|
|
|
|
void
|
|
run_test_case_child(void* raw_params)
|
|
{
|
|
const test_case_params* params =
|
|
static_cast< const test_case_params* >(raw_params);
|
|
|
|
const std::pair< int, int > user = tools::get_required_user(
|
|
params->metadata, params->config);
|
|
if (user.first != -1 && user.second != -1)
|
|
tools::user::drop_privileges(user);
|
|
|
|
// The input 'tp' parameter may be relative and become invalid once
|
|
// we change the current working directory.
|
|
const tools::fs::path absolute_executable = params->executable.to_absolute();
|
|
|
|
// Prepare the test program's arguments. We use dynamic memory and
|
|
// do not care to release it. We are going to die anyway very soon,
|
|
// either due to exec(2) or to exit(3).
|
|
std::vector< std::string > argv;
|
|
argv.push_back(absolute_executable.leaf_name());
|
|
argv.push_back("-r" + params->resfile.str());
|
|
argv.push_back("-s" + absolute_executable.branch_path().str());
|
|
append_to_vector(argv, config_to_args(params->config));
|
|
argv.push_back(params->test_case_name + ":" + params->test_case_part);
|
|
|
|
prepare_child(params->workdir);
|
|
exec_or_exit(absolute_executable, argv);
|
|
}
|
|
|
|
static void
|
|
tokenize_result(const std::string& line, std::string& out_state,
|
|
std::string& out_arg, std::string& out_reason)
|
|
{
|
|
const std::string::size_type pos = line.find_first_of(":(");
|
|
if (pos == std::string::npos) {
|
|
out_state = line;
|
|
out_arg = "";
|
|
out_reason = "";
|
|
} else if (line[pos] == ':') {
|
|
out_state = line.substr(0, pos);
|
|
out_arg = "";
|
|
out_reason = tools::text::trim(line.substr(pos + 1));
|
|
} else if (line[pos] == '(') {
|
|
const std::string::size_type pos2 = line.find("):", pos);
|
|
if (pos2 == std::string::npos)
|
|
throw std::runtime_error("Invalid test case result '" + line +
|
|
"': unclosed optional argument");
|
|
out_state = line.substr(0, pos);
|
|
out_arg = line.substr(pos + 1, pos2 - pos - 1);
|
|
out_reason = tools::text::trim(line.substr(pos2 + 2));
|
|
} else
|
|
std::abort();
|
|
}
|
|
|
|
static impl::test_case_result
|
|
handle_result(const std::string& state, const std::string& arg,
|
|
const std::string& reason)
|
|
{
|
|
assert(state == "passed");
|
|
|
|
if (!arg.empty() || !reason.empty())
|
|
throw std::runtime_error("The test case result '" + state + "' cannot "
|
|
"be accompanied by a reason nor an expected value");
|
|
|
|
return impl::test_case_result(state, -1, reason);
|
|
}
|
|
|
|
static impl::test_case_result
|
|
handle_result_with_reason(const std::string& state, const std::string& arg,
|
|
const std::string& reason)
|
|
{
|
|
assert(state == "expected_death" || state == "expected_failure" ||
|
|
state == "expected_timeout" || state == "failed" || state == "skipped");
|
|
|
|
if (!arg.empty() || reason.empty())
|
|
throw std::runtime_error("The test case result '" + state + "' must "
|
|
"be accompanied by a reason but not by an expected value");
|
|
|
|
return impl::test_case_result(state, -1, reason);
|
|
}
|
|
|
|
static impl::test_case_result
|
|
handle_result_with_reason_and_arg(const std::string& state,
|
|
const std::string& arg,
|
|
const std::string& reason)
|
|
{
|
|
assert(state == "expected_exit" || state == "expected_signal");
|
|
|
|
if (reason.empty())
|
|
throw std::runtime_error("The test case result '" + state + "' must "
|
|
"be accompanied by a reason");
|
|
|
|
int value;
|
|
if (arg.empty()) {
|
|
value = -1;
|
|
} else {
|
|
try {
|
|
value = tools::text::to_type< int >(arg);
|
|
} catch (const std::runtime_error&) {
|
|
throw std::runtime_error("The value '" + arg + "' passed to the '" +
|
|
state + "' state must be an integer");
|
|
}
|
|
}
|
|
|
|
return impl::test_case_result(state, value, reason);
|
|
}
|
|
|
|
} // anonymous namespace
|
|
|
|
detail::atf_tp_reader::atf_tp_reader(std::istream& is) :
|
|
m_is(is)
|
|
{
|
|
}
|
|
|
|
detail::atf_tp_reader::~atf_tp_reader(void)
|
|
{
|
|
}
|
|
|
|
void
|
|
detail::atf_tp_reader::got_tc(
|
|
const std::string& ident ATF_DEFS_ATTRIBUTE_UNUSED,
|
|
const std::map< std::string, std::string >& md ATF_DEFS_ATTRIBUTE_UNUSED)
|
|
{
|
|
}
|
|
|
|
void
|
|
detail::atf_tp_reader::got_eof(void)
|
|
{
|
|
}
|
|
|
|
void
|
|
detail::atf_tp_reader::validate_and_insert(const std::string& name,
|
|
const std::string& value, const size_t lineno,
|
|
std::map< std::string, std::string >& md)
|
|
{
|
|
using tools::parser::parse_error;
|
|
|
|
if (value.empty())
|
|
throw parse_error(lineno, "The value for '" + name +"' cannot be "
|
|
"empty");
|
|
|
|
const std::string ident_regex = "^[_A-Za-z0-9]+$";
|
|
const std::string integer_regex = "^[0-9]+$";
|
|
|
|
if (name == "descr") {
|
|
// Any non-empty value is valid.
|
|
} else if (name == "has.cleanup") {
|
|
try {
|
|
(void)tools::text::to_bool(value);
|
|
} catch (const std::runtime_error&) {
|
|
throw parse_error(lineno, "The has.cleanup property requires a"
|
|
" boolean value");
|
|
}
|
|
} else if (name == "ident") {
|
|
if (!tools::text::match(value, ident_regex))
|
|
throw parse_error(lineno, "The identifier must match " +
|
|
ident_regex + "; was '" + value + "'");
|
|
} else if (name == "require.arch") {
|
|
} else if (name == "require.config") {
|
|
} else if (name == "require.files") {
|
|
} else if (name == "require.machine") {
|
|
} else if (name == "require.memory") {
|
|
try {
|
|
(void)tools::text::to_bytes(value);
|
|
} catch (const std::runtime_error&) {
|
|
throw parse_error(lineno, "The require.memory property requires an "
|
|
"integer value representing an amount of bytes");
|
|
}
|
|
} else if (name == "require.progs") {
|
|
} else if (name == "require.user") {
|
|
} else if (name == "timeout") {
|
|
if (!tools::text::match(value, integer_regex))
|
|
throw parse_error(lineno, "The timeout property requires an integer"
|
|
" value");
|
|
} else if (name == "use.fs") {
|
|
// Deprecated; ignore it.
|
|
} else if (name.size() > 2 && name[0] == 'X' && name[1] == '-') {
|
|
// Any non-empty value is valid.
|
|
} else {
|
|
throw parse_error(lineno, "Unknown property '" + name + "'");
|
|
}
|
|
|
|
md.insert(std::make_pair(name, value));
|
|
}
|
|
|
|
void
|
|
detail::atf_tp_reader::read(void)
|
|
{
|
|
using tools::parser::parse_error;
|
|
using namespace atf_tp;
|
|
|
|
std::pair< size_t, tools::parser::headers_map > hml =
|
|
tools::parser::read_headers(m_is, 1);
|
|
tools::parser::validate_content_type(hml.second,
|
|
"application/X-atf-tp", 1);
|
|
|
|
tokenizer tkz(m_is, hml.first);
|
|
tools::parser::parser< tokenizer > p(tkz);
|
|
|
|
try {
|
|
tools::parser::token t = p.expect(text_type, "property name");
|
|
if (t.text() != "ident")
|
|
throw parse_error(t.lineno(), "First property of a test case "
|
|
"must be 'ident'");
|
|
|
|
std::map< std::string, std::string > props;
|
|
do {
|
|
const std::string name = t.text();
|
|
t = p.expect(colon_type, "`:'");
|
|
const std::string value = tools::text::trim(p.rest_of_line());
|
|
t = p.expect(nl_type, "new line");
|
|
validate_and_insert(name, value, t.lineno(), props);
|
|
|
|
t = p.expect(eof_type, nl_type, text_type, "property name, new "
|
|
"line or eof");
|
|
if (t.type() == nl_type || t.type() == eof_type) {
|
|
const std::map< std::string, std::string >::const_iterator
|
|
iter = props.find("ident");
|
|
if (iter == props.end())
|
|
throw parse_error(t.lineno(), "Test case definition did "
|
|
"not define an 'ident' property");
|
|
ATF_PARSER_CALLBACK(p, got_tc((*iter).second, props));
|
|
props.clear();
|
|
|
|
if (t.type() == nl_type) {
|
|
t = p.expect(text_type, "property name");
|
|
if (t.text() != "ident")
|
|
throw parse_error(t.lineno(), "First property of a "
|
|
"test case must be 'ident'");
|
|
}
|
|
}
|
|
} while (t.type() != eof_type);
|
|
ATF_PARSER_CALLBACK(p, got_eof());
|
|
} catch (const parse_error& pe) {
|
|
p.add_error(pe);
|
|
p.reset(nl_type);
|
|
}
|
|
}
|
|
|
|
impl::test_case_result
|
|
detail::parse_test_case_result(const std::string& line)
|
|
{
|
|
std::string state, arg, reason;
|
|
tokenize_result(line, state, arg, reason);
|
|
|
|
if (state == "expected_death")
|
|
return handle_result_with_reason(state, arg, reason);
|
|
else if (state.compare(0, 13, "expected_exit") == 0)
|
|
return handle_result_with_reason_and_arg(state, arg, reason);
|
|
else if (state.compare(0, 16, "expected_failure") == 0)
|
|
return handle_result_with_reason(state, arg, reason);
|
|
else if (state.compare(0, 15, "expected_signal") == 0)
|
|
return handle_result_with_reason_and_arg(state, arg, reason);
|
|
else if (state.compare(0, 16, "expected_timeout") == 0)
|
|
return handle_result_with_reason(state, arg, reason);
|
|
else if (state == "failed")
|
|
return handle_result_with_reason(state, arg, reason);
|
|
else if (state == "passed")
|
|
return handle_result(state, arg, reason);
|
|
else if (state == "skipped")
|
|
return handle_result_with_reason(state, arg, reason);
|
|
else
|
|
throw std::runtime_error("Unknown test case result type in: " + line);
|
|
}
|
|
|
|
impl::atf_tps_writer::atf_tps_writer(std::ostream& os) :
|
|
m_os(os)
|
|
{
|
|
tools::parser::headers_map hm;
|
|
tools::parser::attrs_map ct_attrs;
|
|
ct_attrs["version"] = "3";
|
|
hm["Content-Type"] =
|
|
tools::parser::header_entry("Content-Type", "application/X-atf-tps",
|
|
ct_attrs);
|
|
tools::parser::write_headers(hm, m_os);
|
|
}
|
|
|
|
void
|
|
impl::atf_tps_writer::info(const std::string& what, const std::string& val)
|
|
{
|
|
m_os << "info: " << what << ", " << val << "\n";
|
|
m_os.flush();
|
|
}
|
|
|
|
void
|
|
impl::atf_tps_writer::ntps(size_t p_ntps)
|
|
{
|
|
m_os << "tps-count: " << p_ntps << "\n";
|
|
m_os.flush();
|
|
}
|
|
|
|
void
|
|
impl::atf_tps_writer::start_tp(const std::string& tp, size_t ntcs)
|
|
{
|
|
m_tpname = tp;
|
|
m_os << "tp-start: " << generate_timestamp() << ", " << tp << ", "
|
|
<< ntcs << "\n";
|
|
m_os.flush();
|
|
}
|
|
|
|
void
|
|
impl::atf_tps_writer::end_tp(const std::string& reason)
|
|
{
|
|
assert(reason.find('\n') == std::string::npos);
|
|
if (reason.empty())
|
|
m_os << "tp-end: " << generate_timestamp() << ", " << m_tpname << "\n";
|
|
else
|
|
m_os << "tp-end: " << generate_timestamp() << ", " << m_tpname
|
|
<< ", " << reason << "\n";
|
|
m_os.flush();
|
|
}
|
|
|
|
void
|
|
impl::atf_tps_writer::start_tc(const std::string& tcname)
|
|
{
|
|
m_tcname = tcname;
|
|
m_os << "tc-start: " << generate_timestamp() << ", " << tcname << "\n";
|
|
m_os.flush();
|
|
}
|
|
|
|
void
|
|
impl::atf_tps_writer::stdout_tc(const std::string& line)
|
|
{
|
|
m_os << "tc-so:" << line << "\n";
|
|
check_stream(m_os);
|
|
m_os.flush();
|
|
check_stream(m_os);
|
|
}
|
|
|
|
void
|
|
impl::atf_tps_writer::stderr_tc(const std::string& line)
|
|
{
|
|
m_os << "tc-se:" << line << "\n";
|
|
check_stream(m_os);
|
|
m_os.flush();
|
|
check_stream(m_os);
|
|
}
|
|
|
|
void
|
|
impl::atf_tps_writer::end_tc(const std::string& state,
|
|
const std::string& reason)
|
|
{
|
|
std::string str = ", " + m_tcname + ", " + state;
|
|
if (!reason.empty())
|
|
str += ", " + reason;
|
|
m_os << "tc-end: " << generate_timestamp() << str << "\n";
|
|
m_os.flush();
|
|
}
|
|
|
|
impl::metadata
|
|
impl::get_metadata(const tools::fs::path& executable,
|
|
const vars_map& config)
|
|
{
|
|
get_metadata_params params(executable, config);
|
|
tools::process::child child =
|
|
tools::process::fork(get_metadata_child,
|
|
tools::process::stream_capture(),
|
|
tools::process::stream_inherit(),
|
|
static_cast< void * >(¶ms));
|
|
|
|
tools::io::pistream outin(child.stdout_fd());
|
|
|
|
metadata_reader parser(outin);
|
|
parser.read();
|
|
|
|
const tools::process::status status = child.wait();
|
|
if (!status.exited() || status.exitstatus() != EXIT_SUCCESS)
|
|
throw tools::parser::format_error("Test program returned failure "
|
|
"exit status for test case list");
|
|
|
|
return metadata(parser.get_tcs());
|
|
}
|
|
|
|
impl::test_case_result
|
|
impl::read_test_case_result(const tools::fs::path& results_path)
|
|
{
|
|
std::ifstream results_file(results_path.c_str());
|
|
if (!results_file)
|
|
throw std::runtime_error("Failed to open " + results_path.str());
|
|
|
|
std::string line, extra_line;
|
|
std::getline(results_file, line);
|
|
if (!results_file.good())
|
|
throw std::runtime_error("Results file is empty");
|
|
|
|
while (std::getline(results_file, extra_line).good())
|
|
line += "<<NEWLINE UNEXPECTED>>" + extra_line;
|
|
|
|
results_file.close();
|
|
|
|
return detail::parse_test_case_result(line);
|
|
}
|
|
|
|
namespace {
|
|
|
|
static volatile bool terminate_poll;
|
|
|
|
static void
|
|
sigchld_handler(const int signo ATF_DEFS_ATTRIBUTE_UNUSED)
|
|
{
|
|
terminate_poll = true;
|
|
}
|
|
|
|
class child_muxer : public tools::io::muxer {
|
|
impl::atf_tps_writer& m_writer;
|
|
|
|
void
|
|
line_callback(const size_t index, const std::string& line)
|
|
{
|
|
switch (index) {
|
|
case 0: m_writer.stdout_tc(line); break;
|
|
case 1: m_writer.stderr_tc(line); break;
|
|
default: std::abort();
|
|
}
|
|
}
|
|
|
|
public:
|
|
child_muxer(const int* fds, const size_t nfds,
|
|
impl::atf_tps_writer& writer) :
|
|
muxer(fds, nfds),
|
|
m_writer(writer)
|
|
{
|
|
}
|
|
};
|
|
|
|
} // anonymous namespace
|
|
|
|
std::pair< std::string, tools::process::status >
|
|
impl::run_test_case(const tools::fs::path& executable,
|
|
const std::string& test_case_name,
|
|
const std::string& test_case_part,
|
|
const vars_map& metadata,
|
|
const vars_map& config,
|
|
const tools::fs::path& resfile,
|
|
const tools::fs::path& workdir,
|
|
atf_tps_writer& writer)
|
|
{
|
|
// TODO: Capture termination signals and deliver them to the subprocess
|
|
// instead. Or maybe do something else; think about it.
|
|
|
|
test_case_params params(executable, test_case_name, test_case_part,
|
|
metadata, config, resfile, workdir);
|
|
tools::process::child child =
|
|
tools::process::fork(run_test_case_child,
|
|
tools::process::stream_capture(),
|
|
tools::process::stream_capture(),
|
|
static_cast< void * >(¶ms));
|
|
|
|
terminate_poll = false;
|
|
|
|
const vars_map::const_iterator iter = metadata.find("timeout");
|
|
assert(iter != metadata.end());
|
|
const unsigned int timeout =
|
|
tools::text::to_type< unsigned int >((*iter).second);
|
|
const pid_t child_pid = child.pid();
|
|
|
|
// Get the input stream of stdout and stderr.
|
|
tools::io::file_handle outfh = child.stdout_fd();
|
|
tools::io::file_handle errfh = child.stderr_fd();
|
|
|
|
bool timed_out = false;
|
|
|
|
// Process the test case's output and multiplex it into our output
|
|
// stream as we read it.
|
|
int fds[2] = {outfh.get(), errfh.get()};
|
|
child_muxer mux(fds, 2, writer);
|
|
try {
|
|
timers::child_timer timeout_timer(timeout, child_pid, terminate_poll);
|
|
signals::signal_programmer sigchld(SIGCHLD, sigchld_handler);
|
|
mux.mux(terminate_poll);
|
|
timed_out = timeout_timer.fired();
|
|
} catch (...) {
|
|
std::abort();
|
|
}
|
|
|
|
::killpg(child_pid, SIGKILL);
|
|
mux.flush();
|
|
tools::process::status status = child.wait();
|
|
|
|
std::string reason;
|
|
|
|
if (timed_out) {
|
|
// Don't assume the child process has been signaled due to the timeout
|
|
// expiration as older versions did. The child process may have exited
|
|
// but we may have timed out due to a subchild process getting stuck.
|
|
reason = "Test case timed out after " + tools::text::to_string(timeout) +
|
|
" " + (timeout == 1 ? "second" : "seconds");
|
|
}
|
|
|
|
return std::make_pair(reason, status);
|
|
}
|