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
941 lines
31 KiB
C++
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);
|
|
}
|