diff options
Diffstat (limited to 'utils/process/executor_test.cpp')
-rw-r--r-- | utils/process/executor_test.cpp | 940 |
1 files changed, 940 insertions, 0 deletions
diff --git a/utils/process/executor_test.cpp b/utils/process/executor_test.cpp new file mode 100644 index 000000000000..13ae69bd44ed --- /dev/null +++ b/utils/process/executor_test.cpp @@ -0,0 +1,940 @@ +// 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); +} |