NetBSD-5.0.2/dist/atf/tools/atf-run.cpp

//
// Automated Testing Framework (atf)
//
// Copyright (c) 2007, 2008 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.
//

#if defined(HAVE_CONFIG_H)
#include "bconfig.h"
#endif

extern "C" {
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
}

#include <cerrno>
#include <cstdlib>
#include <fstream>
#include <iostream>
#include <map>
#include <string>

#include "atf-c++/application.hpp"
#include "atf-c++/atffile.hpp"
#include "atf-c++/config.hpp"
#include "atf-c++/env.hpp"
#include "atf-c++/exceptions.hpp"
#include "atf-c++/formats.hpp"
#include "atf-c++/fs.hpp"
#include "atf-c++/io.hpp"
#include "atf-c++/parser.hpp"
#include "atf-c++/sanity.hpp"
#include "atf-c++/tests.hpp"
#include "atf-c++/text.hpp"

class config : public atf::formats::atf_config_reader {
    atf::tests::vars_map m_vars;

    void
    got_var(const std::string& var, const std::string& name)
    {
        m_vars[var] = name;
    }

public:
    config(std::istream& is) :
        atf::formats::atf_config_reader(is)
    {
    }

    const atf::tests::vars_map&
    get_vars(void)
        const
    {
        return m_vars;
    }
};

class muxer : public atf::formats::atf_tcs_reader {
    atf::fs::path m_tp;
    atf::formats::atf_tps_writer m_writer;

    bool m_inited, m_finalized;
    size_t m_ntcs;
    std::string m_tcname;

    // Counters for the test cases run by the test program.
    size_t m_passed, m_failed, m_skipped;

    void
    got_ntcs(size_t ntcs)
    {
        m_writer.start_tp(m_tp.str(), ntcs);
        m_inited = true;
        if (ntcs == 0)
            throw atf::formats::format_error("Bogus test program: reported "
                                             "0 test cases");
    }

    void
    got_tc_start(const std::string& tcname)
    {
        m_tcname = tcname;
        m_writer.start_tc(tcname);
    }

    void
    got_tc_end(const atf::tests::tcr& tcr)
    {
        const atf::tests::tcr::state& s = tcr.get_state();
        if (s == atf::tests::tcr::passed_state) {
            m_passed++;
        } else if (s == atf::tests::tcr::skipped_state) {
            m_skipped++;
        } else if (s == atf::tests::tcr::failed_state) {
            m_failed++;
        } else
            UNREACHABLE;

        m_writer.end_tc(tcr);
        m_tcname = "";
    }

    void
    got_stdout_line(const std::string& line)
    {
        m_writer.stdout_tc(line);
    }

    void
    got_stderr_line(const std::string& line)
    {
        m_writer.stderr_tc(line);
    }

public:
    muxer(const atf::fs::path& tp, atf::formats::atf_tps_writer& w,
           atf::io::pistream& is) :
        atf::formats::atf_tcs_reader(is),
        m_tp(tp),
        m_writer(w),
        m_inited(false),
        m_finalized(false),
        m_passed(0),
        m_failed(0),
        m_skipped(0)
    {
    }

    size_t
    failed(void)
        const
    {
        return m_failed;
    }

    void
    finalize(const std::string& reason = "")
    {
        PRE(!m_finalized);

        if (!m_inited)
            m_writer.start_tp(m_tp.str(), 0);
        if (!m_tcname.empty()) {
            INV(!reason.empty());
            got_tc_end(atf::tests::tcr(atf::tests::tcr::failed_state,
                                       "Bogus test program"));
        }

        m_writer.end_tp(reason);
        m_finalized = true;
    }

    ~muxer(void)
    {
        // The following is incorrect because we cannot throw an exception
        // from a destructor.  Let's just hope that this never happens.
        PRE(m_finalized);
    }
};

template< class K, class V >
void
merge_maps(std::map< K, V >& dest, const std::map< K, V >& src)
{
    for (typename std::map< K, V >::const_iterator iter = src.begin();
         iter != src.end(); iter++)
        dest[(*iter).first] = (*iter).second;
}

class atf_run : public atf::application::app {
    static const char* m_description;

    atf::tests::vars_map m_atffile_vars;
    atf::tests::vars_map m_cmdline_vars;
    atf::tests::vars_map m_config_vars;

    static atf::tests::vars_map::value_type parse_var(const std::string&);

    void process_option(int, const char*);
    std::string specific_args(void) const;
    options_set specific_options(void) const;

    void parse_vflag(const std::string&);

    void read_one_config(const atf::fs::path&);
    void read_config(const std::string&);
    std::vector< std::string > conf_args(void) const;

    size_t count_tps(std::vector< std::string >) const;

    int run_test(const atf::fs::path&,
                 atf::formats::atf_tps_writer&);
    int run_test_directory(const atf::fs::path&,
                           atf::formats::atf_tps_writer&);
    int run_test_program(const atf::fs::path&,
                         atf::formats::atf_tps_writer&);

    void run_test_program_child(const atf::fs::path&,
                                atf::io::pipe&,
                                atf::io::pipe&,
                                atf::io::pipe&);
    int run_test_program_parent(const atf::fs::path&,
                                atf::formats::atf_tps_writer&,
                                atf::io::pipe&,
                                atf::io::pipe&,
                                atf::io::pipe&,
                                pid_t);

public:
    atf_run(void);

    int main(void);
};

const char* atf_run::m_description =
    "atf-run is a tool that runs tests programs and collects their "
    "results.";

atf_run::atf_run(void) :
    app(m_description, "atf-run(1)", "atf(7)")
{
}

void
atf_run::process_option(int ch, const char* arg)
{
    switch (ch) {
    case 'v':
        parse_vflag(arg);
        break;

    default:
        UNREACHABLE;
    }
}

std::string
atf_run::specific_args(void)
    const
{
    return "[test-program1 .. test-programN]";
}

atf_run::options_set
atf_run::specific_options(void)
    const
{
    using atf::application::option;
    options_set opts;
    opts.insert(option('v', "var=value", "Sets the configuration variable "
                                         "`var' to `value'; overrides "
                                         "values in configuration files"));
    return opts;
}

void
atf_run::parse_vflag(const std::string& str)
{
    if (str.empty())
        throw std::runtime_error("-v requires a non-empty argument");

    std::vector< std::string > ws = atf::text::split(str, "=");
    if (ws.size() == 1 && str[str.length() - 1] == '=') {
        m_cmdline_vars[ws[0]] = "";
    } else {
        if (ws.size() != 2)
            throw std::runtime_error("-v requires an argument of the form "
                                     "var=value");

        m_cmdline_vars[ws[0]] = ws[1];
    }
}

int
atf_run::run_test(const atf::fs::path& tp,
                  atf::formats::atf_tps_writer& w)
{
    atf::fs::file_info fi(tp);

    int errcode;
    if (fi.get_type() == atf::fs::file_info::dir_type)
        errcode = run_test_directory(tp, w);
    else
        errcode = run_test_program(tp, w);
    return errcode;
}

int
atf_run::run_test_directory(const atf::fs::path& tp,
                            atf::formats::atf_tps_writer& w)
{
    atf::atffile af(tp / "Atffile");
    m_atffile_vars = af.conf();

    atf::tests::vars_map oldvars = m_config_vars;
    {
        atf::tests::vars_map::const_iterator iter =
            af.props().find("test-suite");
        INV(iter != af.props().end());
        read_config((*iter).second);
    }

    bool ok = true;
    for (std::vector< std::string >::const_iterator iter = af.tps().begin();
         iter != af.tps().end(); iter++)
        ok &= (run_test(tp / *iter, w) == EXIT_SUCCESS);

    m_config_vars = oldvars;

    return ok ? EXIT_SUCCESS : EXIT_FAILURE;
}

std::vector< std::string >
atf_run::conf_args(void) const
{
    using atf::tests::vars_map;

    atf::tests::vars_map vars;
    std::vector< std::string > args;

    merge_maps(vars, m_atffile_vars);
    merge_maps(vars, m_config_vars);
    merge_maps(vars, m_cmdline_vars);

    for (vars_map::const_iterator i = vars.begin(); i != vars.end(); i++)
        args.push_back("-v" + (*i).first + "=" + (*i).second);

    return args;
}

void
atf_run::run_test_program_child(const atf::fs::path& tp,
                                atf::io::pipe& outpipe,
                                atf::io::pipe& errpipe,
                                atf::io::pipe& respipe)
{
    // Remap stdout and stderr to point to the parent, who will capture
    // everything sent to these.
    outpipe.rend().close();
    outpipe.wend().posix_remap(STDOUT_FILENO);
    errpipe.rend().close();
    errpipe.wend().posix_remap(STDERR_FILENO);

    // Remap the results file descriptor to point to the parent too.
    // We use the 9th one (instead of a bigger one) because shell scripts
    // can only use the [0..9] file descriptors in their redirections.
    respipe.rend().close();
    respipe.wend().posix_remap(9);

    // 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 > confargs = conf_args();
    char** args = new char*[4 + confargs.size()];
    {
        // 0: Program name.
        std::string progname = tp.leaf_name();
        args[0] = new char[progname.length() + 1];
        std::strcpy(args[0], progname.c_str());

        // 1: The file descriptor to which the results will be printed.
        args[1] = new char[4];
        std::strcpy(args[1], "-r9");

        // 2: The directory where the test program lives.
        atf::fs::path bp = tp.branch_path();
        if (!bp.is_absolute())
            bp = bp.to_absolute();
        const char* dir = bp.c_str();
        args[2] = new char[std::strlen(dir) + 3];
        std::strcpy(args[2], "-s");
        std::strcat(args[2], dir);

        // [3..last - 1]: Configuration arguments.
        std::vector< std::string >::size_type i;
        for (i = 0; i < confargs.size(); i++) {
            const char* str = confargs[i].c_str();
            args[3 + i] = new char[std::strlen(str) + 1];
            std::strcpy(args[3 + i], str);
        }

        // Last: Terminator.
        args[3 + i] = NULL;
    }

    // Do the real exec and report any errors to the parent through the
    // only mechanism we can use: stderr.
    // TODO Try to make this fail.
    ::execv(tp.c_str(), args);
    std::cerr << "Failed to execute `" << tp.str() << "': "
              << std::strerror(errno) << std::endl;
    std::exit(EXIT_FAILURE);
}

int
atf_run::run_test_program_parent(const atf::fs::path& tp,
                                 atf::formats::atf_tps_writer& w,
                                 atf::io::pipe& outpipe,
                                 atf::io::pipe& errpipe,
                                 atf::io::pipe& respipe,
                                 pid_t pid)
{
    // Get the file descriptor and input stream of stdout.
    outpipe.wend().close();
    atf::io::unbuffered_istream outin(outpipe.rend());

    // Get the file descriptor and input stream of stderr.
    errpipe.wend().close();
    atf::io::unbuffered_istream errin(errpipe.rend());

    // Get the file descriptor and input stream of the results channel.
    respipe.wend().close();
    atf::io::pistream resin(respipe.rend());

    // Process the test case's output and multiplex it into our output
    // stream as we read it.
    muxer m(tp, w, resin);
    std::string fmterr;
    try {
        m.read(outin, errin);
    } catch (const atf::parser::parse_errors& e) {
        fmterr = "There were errors parsing the output of the test "
                 "program:";
        for (atf::parser::parse_errors::const_iterator iter = e.begin();
             iter != e.end(); iter++) {
            fmterr += " Line " + atf::text::to_string((*iter).first) +
                      ": " + (*iter).second + ".";
        }
    } catch (const atf::formats::format_error& e) {
        fmterr = e.what();
    } catch (...) {
        UNREACHABLE;
    }

    try {
        outin.close();
        errin.close();
        resin.close();
    } catch (...) {
        UNREACHABLE;
    }

    int code, status;
    if (::waitpid(pid, &status, 0) != pid) {
        m.finalize("waitpid(2) on the child process " +
                   atf::text::to_string(pid) + " failed" +
                   (fmterr.empty() ? "" : (".  " + fmterr)));
        code = EXIT_FAILURE;
    } else {
        if (WIFEXITED(status)) {
            code = WEXITSTATUS(status);
            if (m.failed() > 0 && code == EXIT_SUCCESS) {
                code = EXIT_FAILURE;
                m.finalize("Test program returned success but some test "
                           "cases failed" +
                           (fmterr.empty() ? "" : (".  " + fmterr)));
            } else {
                code = fmterr.empty() ? code : EXIT_FAILURE;
                m.finalize(fmterr);
            }
        } else if (WIFSIGNALED(status)) {
            code = EXIT_FAILURE;
            m.finalize("Test program received signal " +
                       atf::text::to_string(WTERMSIG(status)) +
                       (WCOREDUMP(status) ? " (core dumped)" : "") +
                       (fmterr.empty() ? "" : (".  " + fmterr)));
        } else
            throw std::runtime_error
                ("Child process " + atf::text::to_string(pid) +
                 " terminated with an unknown status condition " +
                 atf::text::to_string(status));
    }
    return code;
}

int
atf_run::run_test_program(const atf::fs::path& tp,
                          atf::formats::atf_tps_writer& w)
{
    int errcode;

    atf::io::pipe outpipe, errpipe, respipe;
    pid_t pid = ::fork();
    if (pid == -1) {
        throw atf::system_error("run_test_program",
                                "fork(2) failed", errno);
    } else if (pid == 0) {
        run_test_program_child(tp, outpipe, errpipe, respipe);
        UNREACHABLE;
        errcode = EXIT_FAILURE;
    } else {
        errcode = run_test_program_parent(tp, w, outpipe, errpipe,
                                          respipe, pid);
    }

    return errcode;
}

size_t
atf_run::count_tps(std::vector< std::string > tps)
    const
{
    size_t ntps = 0;

    for (std::vector< std::string >::const_iterator iter = tps.begin();
         iter != tps.end(); iter++) {
        atf::fs::path tp(*iter);
        atf::fs::file_info fi(tp);

        if (fi.get_type() == atf::fs::file_info::dir_type) {
            atf::atffile af = atf::atffile(tp / "Atffile");
            std::vector< std::string > aux = af.tps();
            for (std::vector< std::string >::iterator i2 = aux.begin();
                 i2 != aux.end(); i2++)
                *i2 = (tp / *i2).str();
            ntps += count_tps(aux);
        } else
            ntps++;
    }

    return ntps;
}

void
atf_run::read_one_config(const atf::fs::path& p)
{
    std::ifstream is(p.c_str());
    if (is) {
        config reader(is);
        reader.read();
        merge_maps(m_config_vars, reader.get_vars());
    }
}

void
atf_run::read_config(const std::string& name)
{
    std::vector< atf::fs::path > dirs;
    dirs.push_back(atf::fs::path(atf::config::get("atf_confdir")));
    if (atf::env::has("HOME"))
        dirs.push_back(atf::fs::path(atf::env::get("HOME")) / ".atf");

    m_config_vars.clear();
    for (std::vector< atf::fs::path >::const_iterator iter = dirs.begin();
         iter != dirs.end(); iter++) {
        read_one_config((*iter) / "common.conf");
        read_one_config((*iter) / (name + ".conf"));
    }
}

static
void
call_hook(const std::string& tool, const std::string& hook)
{
    std::string sh = atf::config::get("atf_shell");
    atf::fs::path p = atf::fs::path(atf::config::get("atf_pkgdatadir")) /
                      (tool + ".hooks");
    std::string cmd = sh + " '" + p.str() + "' '" + hook + "'";
    int exitcode = std::system(cmd.c_str());
    if (!WIFEXITED(exitcode) || WEXITSTATUS(exitcode) != EXIT_SUCCESS)
        throw std::runtime_error("Failed to run the '" + hook + "' hook "
                                 "for '" + tool + "'; command was '" +
                                 cmd + "'; exit code " +
                                 atf::text::to_string(exitcode));
}

int
atf_run::main(void)
{
    atf::atffile af(atf::fs::path("Atffile"));
    m_atffile_vars = af.conf();

    std::vector< std::string > tps;
    tps = af.tps();
    if (m_argc >= 1) {
        // TODO: Ensure that the given test names are listed in the
        // Atffile.  Take into account that the file can be using globs.
        tps.clear();
        for (int i = 0; i < m_argc; i++)
            tps.push_back(m_argv[i]);
    }

    // Read configuration data for this test suite.
    {
        atf::tests::vars_map::const_iterator iter =
            af.props().find("test-suite");
        INV(iter != af.props().end());
        read_config((*iter).second);
    }

    atf::formats::atf_tps_writer w(std::cout);
    call_hook("atf-run", "info_start_hook");
    w.ntps(count_tps(tps));

    bool ok = true;
    for (std::vector< std::string >::const_iterator iter = tps.begin();
         iter != tps.end(); iter++)
        ok &= (run_test(atf::fs::path(*iter), w) == EXIT_SUCCESS);

    call_hook("atf-run", "info_end_hook");

    return ok ? EXIT_SUCCESS : EXIT_FAILURE;
}

int
main(int argc, char* const* argv)
{
    return atf_run().run(argc, argv);
}