freebsd-nq/contrib/kyua/utils/process/executor_test.cpp
Brooks Davis b0d29bc47d Import the kyua test framework.
Having kyua in the base system will simplify automated testing in CI and
eliminates bootstrapping issues on new platforms.

The build of kyua is controlled by WITH(OUT)_TESTS_SUPPORT.

Reviewed by:	emaste
Obtained from:	CheriBSD
Sponsored by:	DARPA
Differential Revision:	https://reviews.freebsd.org/D24103
2020-03-23 19:01:23 +00:00

941 lines
31 KiB
C++

// Copyright 2015 The Kyua Authors.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * 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.
// * Neither the name of Google Inc. nor the names of its contributors
// may be used to endorse or promote products derived from this software
// without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 COPYRIGHT
// OWNER 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.
#include "utils/process/executor.ipp"
extern "C" {
#include <sys/types.h>
#include <sys/time.h>
#include <sys/wait.h>
#include <signal.h>
#include <unistd.h>
}
#include <cerrno>
#include <cstdlib>
#include <fstream>
#include <iostream>
#include <vector>
#include <atf-c++.hpp>
#include "utils/datetime.hpp"
#include "utils/defs.hpp"
#include "utils/env.hpp"
#include "utils/format/containers.ipp"
#include "utils/format/macros.hpp"
#include "utils/fs/operations.hpp"
#include "utils/fs/path.hpp"
#include "utils/optional.ipp"
#include "utils/passwd.hpp"
#include "utils/process/status.hpp"
#include "utils/sanity.hpp"
#include "utils/signals/exceptions.hpp"
#include "utils/stacktrace.hpp"
#include "utils/text/exceptions.hpp"
#include "utils/text/operations.ipp"
namespace datetime = utils::datetime;
namespace executor = utils::process::executor;
namespace fs = utils::fs;
namespace passwd = utils::passwd;
namespace process = utils::process;
namespace signals = utils::signals;
namespace text = utils::text;
using utils::none;
using utils::optional;
/// Large timeout for the processes we spawn.
///
/// This number is supposed to be (much) larger than the timeout of the test
/// cases that use it so that children processes are not killed spuriously.
static const datetime::delta infinite_timeout(1000000, 0);
static void do_exit(const int) UTILS_NORETURN;
/// Terminates a subprocess without invoking destructors.
///
/// This is just a simple wrapper over _exit(2) because we cannot use std::exit
/// on exit from a subprocess. The reason is that we do not want to invoke any
/// destructors as otherwise we'd clear up the global executor state by mistake.
/// This wouldn't be a major problem if it wasn't because doing so deletes
/// on-disk files and we want to leave them in place so that the parent process
/// can test for them!
///
/// \param exit_code Code to exit with.
static void
do_exit(const int exit_code)
{
std::cout.flush();
std::cerr.flush();
::_exit(exit_code);
}
/// Subprocess that creates a cookie file in its work directory.
class child_create_cookie {
/// Name of the cookie to create.
const std::string _cookie_name;
public:
/// Constructor.
///
/// \param cookie_name Name of the cookie to create.
child_create_cookie(const std::string& cookie_name) :
_cookie_name(cookie_name)
{
}
/// Runs the subprocess.
void
operator()(const fs::path& /* control_directory */)
UTILS_NORETURN
{
std::cout << "Creating cookie: " << _cookie_name << " (stdout)\n";
std::cerr << "Creating cookie: " << _cookie_name << " (stderr)\n";
atf::utils::create_file(_cookie_name, "");
do_exit(EXIT_SUCCESS);
}
};
static void child_delete_all(const fs::path&) UTILS_NORETURN;
/// Subprocess that deletes all files in the current directory.
///
/// This is intended to validate that the test runs in an empty directory,
/// separate from any control files that the executor may have created.
///
/// \param control_directory Directory where control files separate from the
/// work directory can be placed.
static void
child_delete_all(const fs::path& control_directory)
{
const fs::path cookie = control_directory / "exec_was_called";
std::ofstream control_file(cookie.c_str());
if (!control_file) {
std::cerr << "Failed to create " << cookie << '\n';
std::abort();
}
const int exit_code = ::system("rm *") == -1
? EXIT_FAILURE : EXIT_SUCCESS;
do_exit(exit_code);
}
static void child_dump_unprivileged_user(const fs::path&) UTILS_NORETURN;
/// Subprocess that dumps user configuration.
static void
child_dump_unprivileged_user(const fs::path& /* control_directory */)
{
const passwd::user current_user = passwd::current_user();
std::cout << F("UID = %s\n") % current_user.uid;
do_exit(EXIT_SUCCESS);
}
/// Subprocess that returns a specific exit code.
class child_exit {
/// Exit code to return.
int _exit_code;
public:
/// Constructor.
///
/// \param exit_code Exit code to return.
child_exit(const int exit_code) : _exit_code(exit_code)
{
}
/// Runs the subprocess.
void
operator()(const fs::path& /* control_directory */)
UTILS_NORETURN
{
do_exit(_exit_code);
}
};
static void child_pause(const fs::path&) UTILS_NORETURN;
/// Subprocess that just blocks.
static void
child_pause(const fs::path& /* control_directory */)
{
sigset_t mask;
sigemptyset(&mask);
for (;;) {
::sigsuspend(&mask);
}
std::abort();
}
static void child_print(const fs::path&) UTILS_NORETURN;
/// Subprocess that writes to stdout and stderr.
static void
child_print(const fs::path& /* control_directory */)
{
std::cout << "stdout: some text\n";
std::cerr << "stderr: some other text\n";
do_exit(EXIT_SUCCESS);
}
/// Subprocess that sleeps for a period of time before exiting.
class child_sleep {
/// Seconds to sleep for before termination.
int _seconds;
public:
/// Construtor.
///
/// \param seconds Seconds to sleep for before termination.
child_sleep(const int seconds) : _seconds(seconds)
{
}
/// Runs the subprocess.
void
operator()(const fs::path& /* control_directory */)
UTILS_NORETURN
{
::sleep(_seconds);
do_exit(EXIT_SUCCESS);
}
};
static void child_spawn_blocking_child(const fs::path&) UTILS_NORETURN;
/// Subprocess that spawns a subchild that gets stuck.
///
/// Used by the caller to validate that the whole process tree is terminated
/// when this subprocess is killed.
static void
child_spawn_blocking_child(
const fs::path& /* control_directory */)
{
pid_t pid = ::fork();
if (pid == -1) {
std::cerr << "Cannot fork subprocess\n";
do_exit(EXIT_FAILURE);
} else if (pid == 0) {
for (;;)
::pause();
} else {
const fs::path name = fs::path(utils::getenv("CONTROL_DIR").get()) /
"pid";
std::ofstream pidfile(name.c_str());
if (!pidfile) {
std::cerr << "Failed to create the pidfile\n";
do_exit(EXIT_FAILURE);
}
pidfile << pid;
pidfile.close();
do_exit(EXIT_SUCCESS);
}
}
static void child_validate_isolation(const fs::path&) UTILS_NORETURN;
/// Subprocess that checks if isolate_child() has been called.
static void
child_validate_isolation(const fs::path& /* control_directory */)
{
if (utils::getenv("HOME").get() == "fake-value") {
std::cerr << "HOME not reset\n";
do_exit(EXIT_FAILURE);
}
if (utils::getenv("LANG")) {
std::cerr << "LANG not unset\n";
do_exit(EXIT_FAILURE);
}
do_exit(EXIT_SUCCESS);
}
/// Invokes executor::spawn() with default arguments.
///
/// \param handle The executor on which to invoke spawn().
/// \param args Arguments to the binary.
/// \param timeout Maximum time the program can run for.
/// \param unprivileged_user If set, user to switch to when running the child
/// program.
/// \param stdout_target If not none, file to which to write the stdout of the
/// test case.
/// \param stderr_target If not none, file to which to write the stderr of the
/// test case.
///
/// \return The exec handle for the spawned binary.
template< class Hook >
static executor::exec_handle
do_spawn(executor::executor_handle& handle, Hook hook,
const datetime::delta& timeout = infinite_timeout,
const optional< passwd::user > unprivileged_user = none,
const optional< fs::path > stdout_target = none,
const optional< fs::path > stderr_target = none)
{
const executor::exec_handle exec_handle = handle.spawn< Hook >(
hook, timeout, unprivileged_user, stdout_target, stderr_target);
return exec_handle;
}
/// Checks for a specific exit status in the status of a exit_handle.
///
/// \param exit_status The expected exit status.
/// \param status The value of exit_handle.status().
///
/// \post Terminates the calling test case if the status does not match the
/// required value.
static void
require_exit(const int exit_status, const optional< process::status > status)
{
ATF_REQUIRE(status);
ATF_REQUIRE(status.get().exited());
ATF_REQUIRE_EQ(exit_status, status.get().exitstatus());
}
/// Ensures that a killed process is gone.
///
/// The way we do this is by sending an idempotent signal to the given PID
/// and checking if the signal was delivered. If it was, the process is
/// still alive; if it was not, then it is gone.
///
/// Note that this might be inaccurate for two reasons:
///
/// 1) The system may have spawned a new process with the same pid as
/// our subchild... but in practice, this does not happen because
/// most systems do not immediately reuse pid numbers. If that
/// happens... well, we get a false test failure.
///
/// 2) We ran so fast that even if the process was sent a signal to
/// die, it has not had enough time to process it yet. This is why
/// we retry this a few times.
///
/// \param pid PID of the process to check.
static void
ensure_dead(const pid_t pid)
{
int attempts = 30;
retry:
if (::kill(pid, SIGCONT) != -1 || errno != ESRCH) {
if (attempts > 0) {
std::cout << "Subprocess not dead yet; retrying wait\n";
--attempts;
::usleep(100000);
goto retry;
}
ATF_FAIL(F("The subprocess %s of our child was not killed") % pid);
}
}
ATF_TEST_CASE_WITHOUT_HEAD(integration__run_one);
ATF_TEST_CASE_BODY(integration__run_one)
{
executor::executor_handle handle = executor::setup();
const executor::exec_handle exec_handle = do_spawn(handle, child_exit(41));
executor::exit_handle exit_handle = handle.wait_any();
ATF_REQUIRE_EQ(exec_handle.pid(), exit_handle.original_pid());
require_exit(41, exit_handle.status());
exit_handle.cleanup();
handle.cleanup();
}
ATF_TEST_CASE_WITHOUT_HEAD(integration__run_many);
ATF_TEST_CASE_BODY(integration__run_many)
{
static const std::size_t num_children = 30;
executor::executor_handle handle = executor::setup();
std::size_t total_children = 0;
std::map< int, int > exp_exit_statuses;
std::map< int, datetime::timestamp > exp_start_times;
for (std::size_t i = 0; i < num_children; ++i) {
const datetime::timestamp start_time = datetime::timestamp::from_values(
2014, 12, 8, 9, 40, 0, i);
for (std::size_t j = 0; j < 3; j++) {
const std::size_t id = i * 3 + j;
datetime::set_mock_now(start_time);
const int pid = do_spawn(handle, child_exit(id)).pid();
exp_exit_statuses.insert(std::make_pair(pid, id));
exp_start_times.insert(std::make_pair(pid, start_time));
++total_children;
}
}
for (std::size_t i = 0; i < total_children; ++i) {
const datetime::timestamp end_time = datetime::timestamp::from_values(
2014, 12, 8, 9, 50, 10, i);
datetime::set_mock_now(end_time);
executor::exit_handle exit_handle = handle.wait_any();
const int original_pid = exit_handle.original_pid();
const int exit_status = exp_exit_statuses.find(original_pid)->second;
const datetime::timestamp& start_time = exp_start_times.find(
original_pid)->second;
require_exit(exit_status, exit_handle.status());
ATF_REQUIRE_EQ(start_time, exit_handle.start_time());
ATF_REQUIRE_EQ(end_time, exit_handle.end_time());
exit_handle.cleanup();
ATF_REQUIRE(!atf::utils::file_exists(
exit_handle.stdout_file().str()));
ATF_REQUIRE(!atf::utils::file_exists(
exit_handle.stderr_file().str()));
ATF_REQUIRE(!atf::utils::file_exists(
exit_handle.work_directory().str()));
}
handle.cleanup();
}
ATF_TEST_CASE_WITHOUT_HEAD(integration__parameters_and_output);
ATF_TEST_CASE_BODY(integration__parameters_and_output)
{
executor::executor_handle handle = executor::setup();
const executor::exec_handle exec_handle = do_spawn(handle, child_print);
executor::exit_handle exit_handle = handle.wait_any();
ATF_REQUIRE_EQ(exec_handle.pid(), exit_handle.original_pid());
require_exit(EXIT_SUCCESS, exit_handle.status());
const fs::path stdout_file = exit_handle.stdout_file();
ATF_REQUIRE(atf::utils::compare_file(
stdout_file.str(), "stdout: some text\n"));
const fs::path stderr_file = exit_handle.stderr_file();
ATF_REQUIRE(atf::utils::compare_file(
stderr_file.str(), "stderr: some other text\n"));
exit_handle.cleanup();
ATF_REQUIRE(!fs::exists(stdout_file));
ATF_REQUIRE(!fs::exists(stderr_file));
handle.cleanup();
}
ATF_TEST_CASE_WITHOUT_HEAD(integration__custom_output_files);
ATF_TEST_CASE_BODY(integration__custom_output_files)
{
executor::executor_handle handle = executor::setup();
const fs::path stdout_file("custom-stdout.txt");
const fs::path stderr_file("custom-stderr.txt");
const executor::exec_handle exec_handle = do_spawn(
handle, child_print, infinite_timeout, none,
utils::make_optional(stdout_file),
utils::make_optional(stderr_file));
executor::exit_handle exit_handle = handle.wait_any();
ATF_REQUIRE_EQ(exec_handle.pid(), exit_handle.original_pid());
require_exit(EXIT_SUCCESS, exit_handle.status());
ATF_REQUIRE_EQ(stdout_file, exit_handle.stdout_file());
ATF_REQUIRE_EQ(stderr_file, exit_handle.stderr_file());
exit_handle.cleanup();
handle.cleanup();
// Must compare after cleanup to ensure the files did not get deleted.
ATF_REQUIRE(atf::utils::compare_file(
stdout_file.str(), "stdout: some text\n"));
ATF_REQUIRE(atf::utils::compare_file(
stderr_file.str(), "stderr: some other text\n"));
}
ATF_TEST_CASE_WITHOUT_HEAD(integration__timestamps);
ATF_TEST_CASE_BODY(integration__timestamps)
{
executor::executor_handle handle = executor::setup();
const datetime::timestamp start_time = datetime::timestamp::from_values(
2014, 12, 8, 9, 35, 10, 1000);
const datetime::timestamp end_time = datetime::timestamp::from_values(
2014, 12, 8, 9, 35, 20, 2000);
datetime::set_mock_now(start_time);
do_spawn(handle, child_exit(70));
datetime::set_mock_now(end_time);
executor::exit_handle exit_handle = handle.wait_any();
require_exit(70, exit_handle.status());
ATF_REQUIRE_EQ(start_time, exit_handle.start_time());
ATF_REQUIRE_EQ(end_time, exit_handle.end_time());
exit_handle.cleanup();
handle.cleanup();
}
ATF_TEST_CASE_WITHOUT_HEAD(integration__files);
ATF_TEST_CASE_BODY(integration__files)
{
executor::executor_handle handle = executor::setup();
do_spawn(handle, child_create_cookie("cookie.12345"));
executor::exit_handle exit_handle = handle.wait_any();
ATF_REQUIRE(atf::utils::file_exists(
(exit_handle.work_directory() / "cookie.12345").str()));
exit_handle.cleanup();
ATF_REQUIRE(!atf::utils::file_exists(exit_handle.stdout_file().str()));
ATF_REQUIRE(!atf::utils::file_exists(exit_handle.stderr_file().str()));
ATF_REQUIRE(!atf::utils::file_exists(exit_handle.work_directory().str()));
handle.cleanup();
}
ATF_TEST_CASE_WITHOUT_HEAD(integration__followup);
ATF_TEST_CASE_BODY(integration__followup)
{
executor::executor_handle handle = executor::setup();
(void)handle.spawn(child_create_cookie("cookie.1"), infinite_timeout, none);
executor::exit_handle exit_1_handle = handle.wait_any();
(void)handle.spawn_followup(child_create_cookie("cookie.2"), exit_1_handle,
infinite_timeout);
executor::exit_handle exit_2_handle = handle.wait_any();
ATF_REQUIRE_EQ(exit_1_handle.stdout_file(), exit_2_handle.stdout_file());
ATF_REQUIRE_EQ(exit_1_handle.stderr_file(), exit_2_handle.stderr_file());
ATF_REQUIRE_EQ(exit_1_handle.control_directory(),
exit_2_handle.control_directory());
ATF_REQUIRE_EQ(exit_1_handle.work_directory(),
exit_2_handle.work_directory());
(void)handle.spawn_followup(child_create_cookie("cookie.3"), exit_2_handle,
infinite_timeout);
exit_2_handle.cleanup();
exit_1_handle.cleanup();
executor::exit_handle exit_3_handle = handle.wait_any();
ATF_REQUIRE_EQ(exit_1_handle.stdout_file(), exit_3_handle.stdout_file());
ATF_REQUIRE_EQ(exit_1_handle.stderr_file(), exit_3_handle.stderr_file());
ATF_REQUIRE_EQ(exit_1_handle.control_directory(),
exit_3_handle.control_directory());
ATF_REQUIRE_EQ(exit_1_handle.work_directory(),
exit_3_handle.work_directory());
ATF_REQUIRE(atf::utils::file_exists(
(exit_1_handle.work_directory() / "cookie.1").str()));
ATF_REQUIRE(atf::utils::file_exists(
(exit_1_handle.work_directory() / "cookie.2").str()));
ATF_REQUIRE(atf::utils::file_exists(
(exit_1_handle.work_directory() / "cookie.3").str()));
ATF_REQUIRE(atf::utils::compare_file(
exit_1_handle.stdout_file().str(),
"Creating cookie: cookie.1 (stdout)\n"
"Creating cookie: cookie.2 (stdout)\n"
"Creating cookie: cookie.3 (stdout)\n"));
ATF_REQUIRE(atf::utils::compare_file(
exit_1_handle.stderr_file().str(),
"Creating cookie: cookie.1 (stderr)\n"
"Creating cookie: cookie.2 (stderr)\n"
"Creating cookie: cookie.3 (stderr)\n"));
exit_3_handle.cleanup();
ATF_REQUIRE(!atf::utils::file_exists(exit_1_handle.stdout_file().str()));
ATF_REQUIRE(!atf::utils::file_exists(exit_1_handle.stderr_file().str()));
ATF_REQUIRE(!atf::utils::file_exists(exit_1_handle.work_directory().str()));
handle.cleanup();
}
ATF_TEST_CASE_WITHOUT_HEAD(integration__output_files_always_exist);
ATF_TEST_CASE_BODY(integration__output_files_always_exist)
{
executor::executor_handle handle = executor::setup();
// This test is racy: we specify a very short timeout for the subprocess so
// that we cause the subprocess to exit before it has had time to set up the
// output files. However, for scheduling reasons, the subprocess may
// actually run to completion before the timer triggers. Retry this a few
// times to attempt to catch a "good test".
for (int i = 0; i < 50; i++) {
const executor::exec_handle exec_handle =
do_spawn(handle, child_exit(0), datetime::delta(0, 100000));
executor::exit_handle exit_handle = handle.wait(exec_handle);
ATF_REQUIRE(fs::exists(exit_handle.stdout_file()));
ATF_REQUIRE(fs::exists(exit_handle.stderr_file()));
exit_handle.cleanup();
}
handle.cleanup();
}
ATF_TEST_CASE(integration__timeouts);
ATF_TEST_CASE_HEAD(integration__timeouts)
{
set_md_var("timeout", "60");
}
ATF_TEST_CASE_BODY(integration__timeouts)
{
executor::executor_handle handle = executor::setup();
const executor::exec_handle exec_handle1 =
do_spawn(handle, child_sleep(30), datetime::delta(2, 0));
const executor::exec_handle exec_handle2 =
do_spawn(handle, child_sleep(40), datetime::delta(5, 0));
const executor::exec_handle exec_handle3 =
do_spawn(handle, child_exit(15));
{
executor::exit_handle exit_handle = handle.wait_any();
ATF_REQUIRE_EQ(exec_handle3.pid(), exit_handle.original_pid());
require_exit(15, exit_handle.status());
exit_handle.cleanup();
}
{
executor::exit_handle exit_handle = handle.wait_any();
ATF_REQUIRE_EQ(exec_handle1.pid(), exit_handle.original_pid());
ATF_REQUIRE(!exit_handle.status());
const datetime::delta duration =
exit_handle.end_time() - exit_handle.start_time();
ATF_REQUIRE(duration < datetime::delta(10, 0));
ATF_REQUIRE(duration >= datetime::delta(2, 0));
exit_handle.cleanup();
}
{
executor::exit_handle exit_handle = handle.wait_any();
ATF_REQUIRE_EQ(exec_handle2.pid(), exit_handle.original_pid());
ATF_REQUIRE(!exit_handle.status());
const datetime::delta duration =
exit_handle.end_time() - exit_handle.start_time();
ATF_REQUIRE(duration < datetime::delta(10, 0));
ATF_REQUIRE(duration >= datetime::delta(4, 0));
exit_handle.cleanup();
}
handle.cleanup();
}
ATF_TEST_CASE(integration__unprivileged_user);
ATF_TEST_CASE_HEAD(integration__unprivileged_user)
{
set_md_var("require.config", "unprivileged-user");
set_md_var("require.user", "root");
}
ATF_TEST_CASE_BODY(integration__unprivileged_user)
{
executor::executor_handle handle = executor::setup();
const passwd::user unprivileged_user = passwd::find_user_by_name(
get_config_var("unprivileged-user"));
do_spawn(handle, child_dump_unprivileged_user,
infinite_timeout, utils::make_optional(unprivileged_user));
executor::exit_handle exit_handle = handle.wait_any();
ATF_REQUIRE(atf::utils::compare_file(
exit_handle.stdout_file().str(),
F("UID = %s\n") % unprivileged_user.uid));
exit_handle.cleanup();
handle.cleanup();
}
ATF_TEST_CASE_WITHOUT_HEAD(integration__auto_cleanup);
ATF_TEST_CASE_BODY(integration__auto_cleanup)
{
std::vector< int > pids;
std::vector< fs::path > paths;
{
executor::executor_handle handle = executor::setup();
pids.push_back(do_spawn(handle, child_exit(10)).pid());
pids.push_back(do_spawn(handle, child_exit(20)).pid());
// This invocation is never waited for below. This is intentional: we
// want the destructor to clean the "leaked" test automatically so that
// the clean up of the parent work directory also happens correctly.
pids.push_back(do_spawn(handle, child_pause).pid());
executor::exit_handle exit_handle1 = handle.wait_any();
paths.push_back(exit_handle1.stdout_file());
paths.push_back(exit_handle1.stderr_file());
paths.push_back(exit_handle1.work_directory());
executor::exit_handle exit_handle2 = handle.wait_any();
paths.push_back(exit_handle2.stdout_file());
paths.push_back(exit_handle2.stderr_file());
paths.push_back(exit_handle2.work_directory());
}
for (std::vector< int >::const_iterator iter = pids.begin();
iter != pids.end(); ++iter) {
ensure_dead(*iter);
}
for (std::vector< fs::path >::const_iterator iter = paths.begin();
iter != paths.end(); ++iter) {
ATF_REQUIRE(!atf::utils::file_exists((*iter).str()));
}
}
/// Ensures that interrupting an executor cleans things up correctly.
///
/// This test scenario is tricky. We spawn a master child process that runs the
/// executor code and we send a signal to it externally. The child process
/// spawns a bunch of tests that block indefinitely and tries to wait for their
/// results. When the signal is received, we expect an interrupt_error to be
/// raised, which in turn should clean up all test resources and exit the master
/// child process successfully.
///
/// \param signo Signal to deliver to the executor.
static void
do_signal_handling_test(const int signo)
{
static const char* cookie = "spawned.txt";
const pid_t pid = ::fork();
ATF_REQUIRE(pid != -1);
if (pid == 0) {
static const std::size_t num_children = 3;
optional< fs::path > root_work_directory;
try {
executor::executor_handle handle = executor::setup();
root_work_directory = handle.root_work_directory();
for (std::size_t i = 0; i < num_children; ++i) {
std::cout << "Spawned child number " << i << '\n';
do_spawn(handle, child_pause);
}
std::cout << "Creating " << cookie << " cookie\n";
atf::utils::create_file(cookie, "");
std::cout << "Waiting for subprocess termination\n";
for (std::size_t i = 0; i < num_children; ++i) {
executor::exit_handle exit_handle = handle.wait_any();
// We may never reach this point in the test, but if we do let's
// make sure the subprocess was terminated as expected.
if (exit_handle.status()) {
if (exit_handle.status().get().signaled() &&
exit_handle.status().get().termsig() == SIGKILL) {
// OK.
} else {
std::cerr << "Child exited with unexpected code: "
<< exit_handle.status().get();
std::exit(EXIT_FAILURE);
}
} else {
std::cerr << "Child timed out\n";
std::exit(EXIT_FAILURE);
}
exit_handle.cleanup();
}
std::cerr << "Terminating without reception of signal\n";
std::exit(EXIT_FAILURE);
} catch (const signals::interrupted_error& unused_error) {
std::cerr << "Terminating due to interrupted_error\n";
// We never kill ourselves until the cookie is created, so it is
// guaranteed that the optional root_work_directory has been
// initialized at this point.
if (atf::utils::file_exists(root_work_directory.get().str())) {
// Some cleanup did not happen; error out.
std::exit(EXIT_FAILURE);
} else {
std::exit(EXIT_SUCCESS);
}
}
std::abort();
}
std::cout << "Waiting for " << cookie << " cookie creation\n";
while (!atf::utils::file_exists(cookie)) {
// Wait for processes.
}
ATF_REQUIRE(::unlink(cookie) != -1);
std::cout << "Killing process\n";
ATF_REQUIRE(::kill(pid, signo) != -1);
int status;
std::cout << "Waiting for process termination\n";
ATF_REQUIRE(::waitpid(pid, &status, 0) != -1);
ATF_REQUIRE(WIFEXITED(status));
ATF_REQUIRE_EQ(EXIT_SUCCESS, WEXITSTATUS(status));
}
ATF_TEST_CASE_WITHOUT_HEAD(integration__signal_handling);
ATF_TEST_CASE_BODY(integration__signal_handling)
{
// This test scenario is racy so run it multiple times to have higher
// chances of exposing problems.
const std::size_t rounds = 20;
for (std::size_t i = 0; i < rounds; ++i) {
std::cout << F("Testing round %s\n") % i;
do_signal_handling_test(SIGHUP);
do_signal_handling_test(SIGINT);
do_signal_handling_test(SIGTERM);
}
}
ATF_TEST_CASE_WITHOUT_HEAD(integration__isolate_child_is_called);
ATF_TEST_CASE_BODY(integration__isolate_child_is_called)
{
executor::executor_handle handle = executor::setup();
utils::setenv("HOME", "fake-value");
utils::setenv("LANG", "es_ES");
do_spawn(handle, child_validate_isolation);
executor::exit_handle exit_handle = handle.wait_any();
require_exit(EXIT_SUCCESS, exit_handle.status());
exit_handle.cleanup();
handle.cleanup();
}
ATF_TEST_CASE_WITHOUT_HEAD(integration__process_group_is_terminated);
ATF_TEST_CASE_BODY(integration__process_group_is_terminated)
{
utils::setenv("CONTROL_DIR", fs::current_path().str());
executor::executor_handle handle = executor::setup();
do_spawn(handle, child_spawn_blocking_child);
executor::exit_handle exit_handle = handle.wait_any();
require_exit(EXIT_SUCCESS, exit_handle.status());
exit_handle.cleanup();
handle.cleanup();
if (!fs::exists(fs::path("pid")))
fail("The pid file was not created");
std::ifstream pidfile("pid");
ATF_REQUIRE(pidfile);
pid_t pid;
pidfile >> pid;
pidfile.close();
ensure_dead(pid);
}
ATF_TEST_CASE_WITHOUT_HEAD(integration__prevent_clobbering_control_files);
ATF_TEST_CASE_BODY(integration__prevent_clobbering_control_files)
{
executor::executor_handle handle = executor::setup();
do_spawn(handle, child_delete_all);
executor::exit_handle exit_handle = handle.wait_any();
require_exit(EXIT_SUCCESS, exit_handle.status());
ATF_REQUIRE(atf::utils::file_exists(
(exit_handle.control_directory() / "exec_was_called").str()));
ATF_REQUIRE(!atf::utils::file_exists(
(exit_handle.work_directory() / "exec_was_called").str()));
exit_handle.cleanup();
handle.cleanup();
}
ATF_INIT_TEST_CASES(tcs)
{
ATF_ADD_TEST_CASE(tcs, integration__run_one);
ATF_ADD_TEST_CASE(tcs, integration__run_many);
ATF_ADD_TEST_CASE(tcs, integration__parameters_and_output);
ATF_ADD_TEST_CASE(tcs, integration__custom_output_files);
ATF_ADD_TEST_CASE(tcs, integration__timestamps);
ATF_ADD_TEST_CASE(tcs, integration__files);
ATF_ADD_TEST_CASE(tcs, integration__followup);
ATF_ADD_TEST_CASE(tcs, integration__output_files_always_exist);
ATF_ADD_TEST_CASE(tcs, integration__timeouts);
ATF_ADD_TEST_CASE(tcs, integration__unprivileged_user);
ATF_ADD_TEST_CASE(tcs, integration__auto_cleanup);
ATF_ADD_TEST_CASE(tcs, integration__signal_handling);
ATF_ADD_TEST_CASE(tcs, integration__isolate_child_is_called);
ATF_ADD_TEST_CASE(tcs, integration__process_group_is_terminated);
ATF_ADD_TEST_CASE(tcs, integration__prevent_clobbering_control_files);
}