blob: d2dc3b0c428ba91de612f3ea3e2ceda20ffdb56d [file] [log] [blame]
#include "aos/starter/subprocess.h"
#include <signal.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <ostream>
#include "absl/flags/flag.h"
#include "absl/log/check.h"
#include "absl/log/log.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/str_join.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "aos/configuration.h"
#include "aos/events/shm_event_loop.h"
#include "aos/flatbuffers.h"
#include "aos/ipc_lib/shm_base.h"
#include "aos/testing/path.h"
#include "aos/testing/tmpdir.h"
#include "aos/util/file.h"
namespace aos::starter::testing {
class SubprocessTest : public ::testing::Test {
protected:
SubprocessTest() {
// Nuke the shm dir:
aos::util::UnlinkRecursive(absl::GetFlag(FLAGS_shm_base));
}
};
TEST_F(SubprocessTest, CaptureOutputs) {
const std::string config_file =
::aos::testing::ArtifactPath("aos/events/pingpong_config.json");
aos::FlatbufferDetachedBuffer<aos::Configuration> config =
aos::configuration::ReadConfig(config_file);
aos::ShmEventLoop event_loop(&config.message());
bool observed_stopped = false;
Application echo_stdout(
"echo", "echo", &event_loop, [&observed_stopped, &echo_stdout]() {
if (echo_stdout.status() == aos::starter::State::STOPPED) {
observed_stopped = true;
}
});
ASSERT_FALSE(echo_stdout.autorestart());
echo_stdout.set_args({"abcdef"});
echo_stdout.set_capture_stdout(true);
echo_stdout.set_capture_stderr(true);
echo_stdout.Start();
aos::TimerHandler *exit_timer =
event_loop.AddTimer([&event_loop]() { event_loop.Exit(); });
event_loop.OnRun([&event_loop, exit_timer]() {
// Note: we are using the backup poll in this test to capture SIGCHLD. This
// runs at 1 hz, so make sure we let it run at least once.
exit_timer->Schedule(event_loop.monotonic_now() +
std::chrono::milliseconds(1500));
});
event_loop.Run();
ASSERT_EQ("abcdef\n", echo_stdout.GetStdout());
ASSERT_TRUE(echo_stdout.GetStderr().empty());
EXPECT_TRUE(observed_stopped);
EXPECT_EQ(aos::starter::State::STOPPED, echo_stdout.status());
observed_stopped = false;
// Run again, the output should've been cleared.
echo_stdout.set_args({"ghijkl"});
echo_stdout.Start();
event_loop.Run();
ASSERT_EQ("ghijkl\n", echo_stdout.GetStdout());
EXPECT_TRUE(observed_stopped);
}
TEST_F(SubprocessTest, CaptureStderr) {
const std::string config_file =
::aos::testing::ArtifactPath("aos/events/pingpong_config.json");
aos::FlatbufferDetachedBuffer<aos::Configuration> config =
aos::configuration::ReadConfig(config_file);
aos::ShmEventLoop event_loop(&config.message());
bool observed_stopped = false;
Application echo_stderr(
"echo", "sh", &event_loop, [&observed_stopped, &echo_stderr]() {
if (echo_stderr.status() == aos::starter::State::STOPPED) {
observed_stopped = true;
}
});
echo_stderr.set_args({"-c", "echo abcdef >&2"});
echo_stderr.set_capture_stdout(true);
echo_stderr.set_capture_stderr(true);
echo_stderr.Start();
// Note: we are using the backup poll in this test to capture SIGCHLD. This
// runs at 1 hz, so make sure we let it run at least once.
event_loop.AddTimer([&event_loop]() { event_loop.Exit(); })
->Schedule(event_loop.monotonic_now() + std::chrono::milliseconds(1500));
event_loop.Run();
ASSERT_EQ("abcdef\n", echo_stderr.GetStderr());
ASSERT_TRUE(echo_stderr.GetStdout().empty());
ASSERT_TRUE(observed_stopped);
ASSERT_EQ(aos::starter::State::STOPPED, echo_stderr.status());
}
// Checks that when a child application crashing results in the starter printing
// out its own version by default.
TEST_F(SubprocessTest, PrintNoTimingReportVersionString) {
const std::string config_file =
::aos::testing::ArtifactPath("aos/events/pingpong_config.json");
::testing::internal::CaptureStderr();
// Set up application without quiet flag active
aos::FlatbufferDetachedBuffer<aos::Configuration> config =
aos::configuration::ReadConfig(config_file);
aos::ShmEventLoop event_loop(&config.message());
event_loop.SetVersionString("version_string");
bool observed_stopped = false;
Application error_out(
"false", "bash", &event_loop,
[&observed_stopped, &error_out]() {
if (error_out.status() == aos::starter::State::STOPPED) {
observed_stopped = true;
}
},
Application::QuietLogging::kNo);
error_out.set_args({"-c", "sleep 3; false"});
error_out.Start();
aos::TimerHandler *exit_timer =
event_loop.AddTimer([&event_loop]() { event_loop.Exit(); });
event_loop.OnRun([&event_loop, exit_timer]() {
exit_timer->Schedule(event_loop.monotonic_now() +
std::chrono::milliseconds(5000));
});
event_loop.Run();
// Ensure presence of logs without quiet flag
std::string output = ::testing::internal::GetCapturedStderr();
std::string expected = "starter version 'version_string'";
ASSERT_TRUE(output.find(expected) != std::string::npos) << output;
EXPECT_TRUE(observed_stopped);
EXPECT_EQ(aos::starter::State::STOPPED, error_out.status());
}
TEST_F(SubprocessTest, PrintFailedToStartVersionString) {
const std::string config_file =
::aos::testing::ArtifactPath("aos/events/pingpong_config.json");
::testing::internal::CaptureStderr();
// Set up application without quiet flag active
aos::FlatbufferDetachedBuffer<aos::Configuration> config =
aos::configuration::ReadConfig(config_file);
aos::ShmEventLoop event_loop(&config.message());
event_loop.SetVersionString("version_string");
bool observed_stopped = false;
Application error_out(
"false", "false", &event_loop,
[&observed_stopped, &error_out]() {
if (error_out.status() == aos::starter::State::STOPPED) {
observed_stopped = true;
}
},
Application::QuietLogging::kNo);
error_out.Start();
aos::TimerHandler *exit_timer =
event_loop.AddTimer([&event_loop]() { event_loop.Exit(); });
event_loop.OnRun([&event_loop, exit_timer]() {
exit_timer->Schedule(event_loop.monotonic_now() +
std::chrono::milliseconds(1500));
});
event_loop.Run();
// Ensure presence of logs without quiet flag
std::string output = ::testing::internal::GetCapturedStderr();
std::string expected = "starter version 'version_string'";
ASSERT_TRUE(output.find(expected) != std::string::npos) << output;
EXPECT_TRUE(observed_stopped);
EXPECT_EQ(aos::starter::State::STOPPED, error_out.status());
}
TEST_F(SubprocessTest, UnactiveQuietFlag) {
const std::string config_file =
::aos::testing::ArtifactPath("aos/events/pingpong_config.json");
::testing::internal::CaptureStderr();
// Set up application without quiet flag active
aos::FlatbufferDetachedBuffer<aos::Configuration> config =
aos::configuration::ReadConfig(config_file);
aos::ShmEventLoop event_loop(&config.message());
bool observed_stopped = false;
Application error_out(
"false", "false", &event_loop,
[&observed_stopped, &error_out]() {
if (error_out.status() == aos::starter::State::STOPPED) {
observed_stopped = true;
}
},
Application::QuietLogging::kNo);
ASSERT_FALSE(error_out.autorestart());
error_out.Start();
aos::TimerHandler *exit_timer =
event_loop.AddTimer([&event_loop]() { event_loop.Exit(); });
event_loop.OnRun([&event_loop, exit_timer]() {
exit_timer->Schedule(event_loop.monotonic_now() +
std::chrono::milliseconds(1500));
});
event_loop.Run();
// Ensure presence of logs without quiet flag
std::string output = ::testing::internal::GetCapturedStderr();
std::string expectedStart = "Failed to start 'false'";
std::string expectedRun = "exited unexpectedly with status";
ASSERT_TRUE(output.find(expectedStart) != std::string::npos ||
output.find(expectedRun) != std::string::npos)
<< output;
EXPECT_TRUE(observed_stopped);
EXPECT_EQ(aos::starter::State::STOPPED, error_out.status());
}
TEST_F(SubprocessTest, ActiveQuietFlag) {
const std::string config_file =
::aos::testing::ArtifactPath("aos/events/pingpong_config.json");
::testing::internal::CaptureStderr();
// Set up application with quiet flag active
aos::FlatbufferDetachedBuffer<aos::Configuration> config =
aos::configuration::ReadConfig(config_file);
aos::ShmEventLoop event_loop(&config.message());
bool observed_stopped = false;
Application error_out(
"false", "false", &event_loop,
[&observed_stopped, &error_out]() {
if (error_out.status() == aos::starter::State::STOPPED) {
observed_stopped = true;
}
},
Application::QuietLogging::kYes);
ASSERT_FALSE(error_out.autorestart());
error_out.Start();
aos::TimerHandler *exit_timer =
event_loop.AddTimer([&event_loop]() { event_loop.Exit(); });
event_loop.OnRun([&event_loop, exit_timer]() {
exit_timer->Schedule(event_loop.monotonic_now() +
std::chrono::milliseconds(1500));
});
event_loop.Run();
// Ensure lack of logs with quiet flag
ASSERT_TRUE(::testing::internal::GetCapturedStderr().empty());
EXPECT_TRUE(observed_stopped);
EXPECT_EQ(aos::starter::State::STOPPED, error_out.status());
}
// Tests that Nothing Badâ„¢ happens if the event loop outlives the Application.
//
// Note that this is a bit of a hope test, as there is no guarantee that we
// will trigger a crash even if the resources tied to the event loop in the
// aos::Application aren't properly released.
TEST_F(SubprocessTest, ShortLivedApp) {
const std::string config_file =
::aos::testing::ArtifactPath("aos/events/pingpong_config.json");
aos::FlatbufferDetachedBuffer<aos::Configuration> config =
aos::configuration::ReadConfig(config_file);
aos::ShmEventLoop event_loop(&config.message());
auto application =
std::make_unique<Application>("sleep", "sleep", &event_loop, []() {});
application->set_args({"10"});
application->Start();
pid_t pid = application->get_pid();
int ticks = 0;
aos::TimerHandler *exit_timer = event_loop.AddTimer([&event_loop, &ticks,
&application, pid]() {
ticks++;
if (application && application->status() == aos::starter::State::RUNNING) {
// Kill the application, it will autorestart.
kill(pid, SIGTERM);
application.reset();
}
// event loop lives for longer.
if (ticks >= 5) {
// Now we exit.
event_loop.Exit();
}
});
event_loop.OnRun([&event_loop, exit_timer]() {
exit_timer->Schedule(event_loop.monotonic_now(),
std::chrono::milliseconds(1000));
});
event_loop.Run();
}
// Test that if the binary changes out from under us that we note it in the
// FileState.
TEST_F(SubprocessTest, ChangeBinaryContents) {
const std::string config_file =
::aos::testing::ArtifactPath("aos/events/pingpong_config.json");
aos::FlatbufferDetachedBuffer<aos::Configuration> config =
aos::configuration::ReadConfig(config_file);
aos::ShmEventLoop event_loop(&config.message());
// Create a local copy of the sleep binary so that we can delete it.
const std::filesystem::path full_executable_path =
absl::StrCat(aos::testing::TestTmpDir(), "/", "sleep_binary");
aos::util::WriteStringToFileOrDie(
full_executable_path.native(),
aos::util::ReadFileToStringOrDie(ResolvePath("sleep").native()), S_IRWXU);
const std::filesystem::path executable_name =
absl::StrCat(aos::testing::TestTmpDir(), "/", "sleep_symlink");
// Create a symlink that points to the actual binary, and test that a
// Creating a symlink in particular lets us ensure that our logic actually
// pays attention to the target file that we are running rather than the
// symlink itself (it also saves us having to cp a binary somewhere where we
// can overwrite it).
std::filesystem::create_symlink(full_executable_path.native(),
executable_name);
// Wait until we are running, go through and test that various variations in
// file state result in the expected behavior, and then exit.
Application sleep(
"sleep", executable_name.native(), &event_loop,
[&sleep, &event_loop, executable_name, full_executable_path]() {
switch (sleep.status()) {
case aos::starter::State::RUNNING:
EXPECT_EQ(aos::starter::FileState::NO_CHANGE,
sleep.UpdateFileState());
// Delete the symlink; this should have no effect, because the
// Application class should be looking at the original path.
std::filesystem::remove(executable_name);
EXPECT_EQ(aos::starter::FileState::NO_CHANGE,
sleep.UpdateFileState());
// Delete the executable; it should be changed.
std::filesystem::remove(full_executable_path);
EXPECT_EQ(aos::starter::FileState::CHANGED,
sleep.UpdateFileState());
// Replace the executable itself; it should be changed.
aos::util::WriteStringToFileOrDie(full_executable_path.native(),
"abcdef");
EXPECT_EQ(aos::starter::FileState::CHANGED,
sleep.UpdateFileState());
// Terminate.
event_loop.Exit();
break;
case aos::starter::State::WAITING:
case aos::starter::State::STARTING:
case aos::starter::State::STOPPING:
case aos::starter::State::STOPPED:
EXPECT_EQ(aos::starter::FileState::NOT_RUNNING,
sleep.UpdateFileState());
break;
}
});
ASSERT_FALSE(sleep.autorestart());
// Ensure that the subprocess will run longer than we care about (we just call
// Terminate() below to stop it).
sleep.set_args({"1000"});
sleep.Start();
aos::TimerHandler *exit_timer = event_loop.AddTimer([&event_loop]() {
event_loop.Exit();
FAIL() << "We should have already exited.";
});
event_loop.OnRun([&event_loop, exit_timer]() {
exit_timer->Schedule(event_loop.monotonic_now() +
std::chrono::milliseconds(5000));
});
event_loop.Run();
sleep.Terminate();
}
class ResolvePathTest : public ::testing::Test {
protected:
ResolvePathTest() {
// Before doing anything else,
if (getenv("PATH") != nullptr) {
original_path_ = getenv("PATH");
PCHECK(0 == unsetenv("PATH"));
}
}
~ResolvePathTest() {
if (!original_path_.empty()) {
PCHECK(0 == setenv("PATH", original_path_.c_str(), /*overwrite=*/1));
} else {
PCHECK(0 == unsetenv("PATH"));
}
}
std::filesystem::path GetLocalPath(const std::string filename) {
return absl::StrCat(aos::testing::TestTmpDir(), "/", filename);
}
std::filesystem::path CreateFile(const std::string filename) {
const std::filesystem::path file = GetLocalPath(filename);
VLOG(2) << "Creating file at " << file;
util::WriteStringToFileOrDie(file.native(), "contents");
return file;
}
void SetPath(const std::vector<std::string> &path) {
PCHECK(0 ==
setenv("PATH", absl::StrJoin(path, ":").c_str(), /*overwrite=*/1));
}
// Keep track of original PATH environment variable so that we can restore
// it.
std::string original_path_;
};
// Tests that we can resolve paths when there is no PATH environment variable.
TEST_F(ResolvePathTest, ResolveWithUnsetPath) {
const std::filesystem::path local_echo = CreateFile("echo");
// Because the default path will be in /bin and /usr/bin (typically), we have
// to choose some utility that we can reasonably expect to be available in the
// test environment.
const std::filesystem::path echo_path = ResolvePath("echo");
EXPECT_THAT((std::vector<std::string>{"/bin/echo", "/usr/bin/echo"}),
::testing::Contains(echo_path.native()));
// Test that a file with /'s in the name ignores the PATH.
const std::filesystem::path local_echo_path =
ResolvePath(local_echo.native());
EXPECT_EQ(local_echo_path, local_echo);
}
// Test that when the PATH environment variable is set that we can use it.
TEST_F(ResolvePathTest, ResolveWithPath) {
const std::filesystem::path local_folder = GetLocalPath("bin/");
const std::filesystem::path local_folder2 = GetLocalPath("bin2/");
aos::util::MkdirP(local_folder.native(), S_IRWXU);
aos::util::MkdirP(local_folder2.native(), S_IRWXU);
SetPath({local_folder, local_folder2});
const std::filesystem::path binary = CreateFile("bin/binary");
const std::filesystem::path duplicate_binary = CreateFile("bin2/binary");
const std::filesystem::path other_name = CreateFile("bin2/other_name");
EXPECT_EQ(binary, ResolvePath("binary"));
EXPECT_EQ(other_name, ResolvePath("other_name"));
// And check that if we specify the full path for the duplicate binary that we
// can find it.
EXPECT_EQ(duplicate_binary, ResolvePath(duplicate_binary.native()));
}
// Test that we fail to find non-existent files.
TEST_F(ResolvePathTest, DieOnFakeFile) {
// Fail to find something that searches the PATH.
SetPath({"foo", "bar"});
EXPECT_DEATH(ResolvePath("fake_file"), "Unable to resolve");
// Fail to find a local file.
EXPECT_DEATH(ResolvePath("./fake_file"), "./fake_file does not exist");
}
} // namespace aos::starter::testing