James Kuszmaul | d42edb4 | 2022-01-07 18:00:16 -0800 | [diff] [blame] | 1 | #include "aos/starter/subprocess.h" |
| 2 | |
Adam Snaider | 70deaf2 | 2023-08-11 13:58:34 -0700 | [diff] [blame] | 3 | #include <signal.h> |
| 4 | #include <sys/types.h> |
| 5 | |
James Kuszmaul | 37a56af | 2023-07-29 15:15:16 -0700 | [diff] [blame^] | 6 | #include "absl/strings/str_join.h" |
| 7 | #include "gmock/gmock.h" |
Philipp Schrader | 790cb54 | 2023-07-05 21:06:52 -0700 | [diff] [blame] | 8 | #include "gtest/gtest.h" |
| 9 | |
James Kuszmaul | d42edb4 | 2022-01-07 18:00:16 -0800 | [diff] [blame] | 10 | #include "aos/events/shm_event_loop.h" |
| 11 | #include "aos/testing/path.h" |
| 12 | #include "aos/testing/tmpdir.h" |
| 13 | #include "aos/util/file.h" |
James Kuszmaul | d42edb4 | 2022-01-07 18:00:16 -0800 | [diff] [blame] | 14 | |
| 15 | namespace aos::starter::testing { |
| 16 | |
| 17 | class SubprocessTest : public ::testing::Test { |
| 18 | protected: |
| 19 | SubprocessTest() : shm_dir_(aos::testing::TestTmpDir() + "/aos") { |
| 20 | FLAGS_shm_base = shm_dir_; |
| 21 | |
| 22 | // Nuke the shm dir: |
| 23 | aos::util::UnlinkRecursive(shm_dir_); |
| 24 | } |
| 25 | |
| 26 | gflags::FlagSaver flag_saver_; |
| 27 | std::string shm_dir_; |
| 28 | }; |
| 29 | |
| 30 | TEST_F(SubprocessTest, CaptureOutputs) { |
| 31 | const std::string config_file = |
| 32 | ::aos::testing::ArtifactPath("aos/events/pingpong_config.json"); |
| 33 | |
| 34 | aos::FlatbufferDetachedBuffer<aos::Configuration> config = |
| 35 | aos::configuration::ReadConfig(config_file); |
| 36 | aos::ShmEventLoop event_loop(&config.message()); |
| 37 | bool observed_stopped = false; |
| 38 | Application echo_stdout( |
| 39 | "echo", "echo", &event_loop, [&observed_stopped, &echo_stdout]() { |
| 40 | if (echo_stdout.status() == aos::starter::State::STOPPED) { |
| 41 | observed_stopped = true; |
| 42 | } |
| 43 | }); |
| 44 | ASSERT_FALSE(echo_stdout.autorestart()); |
| 45 | echo_stdout.set_args({"abcdef"}); |
| 46 | echo_stdout.set_capture_stdout(true); |
| 47 | echo_stdout.set_capture_stderr(true); |
| 48 | |
| 49 | echo_stdout.Start(); |
| 50 | aos::TimerHandler *exit_timer = |
| 51 | event_loop.AddTimer([&event_loop]() { event_loop.Exit(); }); |
| 52 | event_loop.OnRun([&event_loop, exit_timer]() { |
Austin Schuh | 63851a4 | 2022-05-16 13:31:37 -0700 | [diff] [blame] | 53 | // Note: we are using the backup poll in this test to capture SIGCHLD. This |
| 54 | // runs at 1 hz, so make sure we let it run at least once. |
Philipp Schrader | a671252 | 2023-07-05 20:25:11 -0700 | [diff] [blame] | 55 | exit_timer->Schedule(event_loop.monotonic_now() + |
| 56 | std::chrono::milliseconds(1500)); |
James Kuszmaul | d42edb4 | 2022-01-07 18:00:16 -0800 | [diff] [blame] | 57 | }); |
| 58 | |
| 59 | event_loop.Run(); |
| 60 | |
James Kuszmaul | d42edb4 | 2022-01-07 18:00:16 -0800 | [diff] [blame] | 61 | ASSERT_EQ("abcdef\n", echo_stdout.GetStdout()); |
| 62 | ASSERT_TRUE(echo_stdout.GetStderr().empty()); |
| 63 | EXPECT_TRUE(observed_stopped); |
| 64 | EXPECT_EQ(aos::starter::State::STOPPED, echo_stdout.status()); |
| 65 | |
| 66 | observed_stopped = false; |
| 67 | |
| 68 | // Run again, the output should've been cleared. |
| 69 | echo_stdout.set_args({"ghijkl"}); |
| 70 | echo_stdout.Start(); |
| 71 | event_loop.Run(); |
| 72 | ASSERT_EQ("ghijkl\n", echo_stdout.GetStdout()); |
| 73 | EXPECT_TRUE(observed_stopped); |
| 74 | } |
| 75 | |
| 76 | TEST_F(SubprocessTest, CaptureStderr) { |
| 77 | const std::string config_file = |
| 78 | ::aos::testing::ArtifactPath("aos/events/pingpong_config.json"); |
| 79 | |
| 80 | aos::FlatbufferDetachedBuffer<aos::Configuration> config = |
| 81 | aos::configuration::ReadConfig(config_file); |
| 82 | aos::ShmEventLoop event_loop(&config.message()); |
| 83 | bool observed_stopped = false; |
| 84 | Application echo_stderr( |
| 85 | "echo", "sh", &event_loop, [&observed_stopped, &echo_stderr]() { |
| 86 | if (echo_stderr.status() == aos::starter::State::STOPPED) { |
| 87 | observed_stopped = true; |
| 88 | } |
| 89 | }); |
| 90 | echo_stderr.set_args({"-c", "echo abcdef >&2"}); |
| 91 | echo_stderr.set_capture_stdout(true); |
| 92 | echo_stderr.set_capture_stderr(true); |
| 93 | |
| 94 | echo_stderr.Start(); |
Austin Schuh | 63851a4 | 2022-05-16 13:31:37 -0700 | [diff] [blame] | 95 | // Note: we are using the backup poll in this test to capture SIGCHLD. This |
| 96 | // runs at 1 hz, so make sure we let it run at least once. |
James Kuszmaul | d42edb4 | 2022-01-07 18:00:16 -0800 | [diff] [blame] | 97 | event_loop.AddTimer([&event_loop]() { event_loop.Exit(); }) |
Philipp Schrader | a671252 | 2023-07-05 20:25:11 -0700 | [diff] [blame] | 98 | ->Schedule(event_loop.monotonic_now() + std::chrono::milliseconds(1500)); |
James Kuszmaul | d42edb4 | 2022-01-07 18:00:16 -0800 | [diff] [blame] | 99 | |
| 100 | event_loop.Run(); |
| 101 | |
| 102 | ASSERT_EQ("abcdef\n", echo_stderr.GetStderr()); |
| 103 | ASSERT_TRUE(echo_stderr.GetStdout().empty()); |
| 104 | ASSERT_TRUE(observed_stopped); |
| 105 | ASSERT_EQ(aos::starter::State::STOPPED, echo_stderr.status()); |
| 106 | } |
| 107 | |
payton.rehl | 2841b1c | 2023-05-25 17:23:55 -0700 | [diff] [blame] | 108 | TEST_F(SubprocessTest, UnactiveQuietFlag) { |
| 109 | const std::string config_file = |
| 110 | ::aos::testing::ArtifactPath("aos/events/pingpong_config.json"); |
| 111 | |
| 112 | ::testing::internal::CaptureStderr(); |
| 113 | |
| 114 | // Set up application without quiet flag active |
| 115 | aos::FlatbufferDetachedBuffer<aos::Configuration> config = |
| 116 | aos::configuration::ReadConfig(config_file); |
| 117 | aos::ShmEventLoop event_loop(&config.message()); |
| 118 | bool observed_stopped = false; |
| 119 | Application error_out( |
| 120 | "false", "false", &event_loop, |
| 121 | [&observed_stopped, &error_out]() { |
| 122 | if (error_out.status() == aos::starter::State::STOPPED) { |
| 123 | observed_stopped = true; |
| 124 | } |
| 125 | }, |
| 126 | Application::QuietLogging::kNo); |
| 127 | ASSERT_FALSE(error_out.autorestart()); |
| 128 | |
| 129 | error_out.Start(); |
| 130 | aos::TimerHandler *exit_timer = |
| 131 | event_loop.AddTimer([&event_loop]() { event_loop.Exit(); }); |
| 132 | event_loop.OnRun([&event_loop, exit_timer]() { |
| 133 | exit_timer->Schedule(event_loop.monotonic_now() + |
| 134 | std::chrono::milliseconds(1500)); |
| 135 | }); |
| 136 | |
| 137 | event_loop.Run(); |
| 138 | |
| 139 | // Ensure presence of logs without quiet flag |
| 140 | std::string output = ::testing::internal::GetCapturedStderr(); |
| 141 | std::string expectedStart = "Failed to start 'false'"; |
| 142 | std::string expectedRun = "exited unexpectedly with status"; |
| 143 | |
| 144 | ASSERT_TRUE(output.find(expectedStart) != std::string::npos || |
| 145 | output.find(expectedRun) != std::string::npos); |
| 146 | EXPECT_TRUE(observed_stopped); |
| 147 | EXPECT_EQ(aos::starter::State::STOPPED, error_out.status()); |
| 148 | } |
| 149 | |
| 150 | TEST_F(SubprocessTest, ActiveQuietFlag) { |
| 151 | const std::string config_file = |
| 152 | ::aos::testing::ArtifactPath("aos/events/pingpong_config.json"); |
| 153 | |
| 154 | ::testing::internal::CaptureStderr(); |
| 155 | |
| 156 | // Set up application with quiet flag active |
| 157 | aos::FlatbufferDetachedBuffer<aos::Configuration> config = |
| 158 | aos::configuration::ReadConfig(config_file); |
| 159 | aos::ShmEventLoop event_loop(&config.message()); |
| 160 | bool observed_stopped = false; |
| 161 | Application error_out( |
| 162 | "false", "false", &event_loop, |
| 163 | [&observed_stopped, &error_out]() { |
| 164 | if (error_out.status() == aos::starter::State::STOPPED) { |
| 165 | observed_stopped = true; |
| 166 | } |
| 167 | }, |
| 168 | Application::QuietLogging::kYes); |
| 169 | ASSERT_FALSE(error_out.autorestart()); |
| 170 | |
| 171 | error_out.Start(); |
| 172 | aos::TimerHandler *exit_timer = |
| 173 | event_loop.AddTimer([&event_loop]() { event_loop.Exit(); }); |
| 174 | event_loop.OnRun([&event_loop, exit_timer]() { |
| 175 | exit_timer->Schedule(event_loop.monotonic_now() + |
| 176 | std::chrono::milliseconds(1500)); |
| 177 | }); |
| 178 | |
| 179 | event_loop.Run(); |
| 180 | |
| 181 | // Ensure lack of logs with quiet flag |
| 182 | ASSERT_TRUE(::testing::internal::GetCapturedStderr().empty()); |
| 183 | EXPECT_TRUE(observed_stopped); |
| 184 | EXPECT_EQ(aos::starter::State::STOPPED, error_out.status()); |
| 185 | } |
| 186 | |
Adam Snaider | 70deaf2 | 2023-08-11 13:58:34 -0700 | [diff] [blame] | 187 | // Tests that Nothing Badâ„¢ happens if the event loop outlives the Application. |
| 188 | // |
| 189 | // Note that this is a bit of a hope test, as there is no guarantee that we |
| 190 | // will trigger a crash even if the resources tied to the event loop in the |
| 191 | // aos::Application aren't properly released. |
| 192 | TEST_F(SubprocessTest, ShortLivedApp) { |
| 193 | const std::string config_file = |
| 194 | ::aos::testing::ArtifactPath("aos/events/pingpong_config.json"); |
| 195 | |
| 196 | aos::FlatbufferDetachedBuffer<aos::Configuration> config = |
| 197 | aos::configuration::ReadConfig(config_file); |
| 198 | aos::ShmEventLoop event_loop(&config.message()); |
| 199 | |
| 200 | auto application = |
| 201 | std::make_unique<Application>("sleep", "sleep", &event_loop, []() {}); |
| 202 | application->set_args({"10"}); |
| 203 | application->Start(); |
| 204 | pid_t pid = application->get_pid(); |
| 205 | |
| 206 | int ticks = 0; |
| 207 | aos::TimerHandler *exit_timer = event_loop.AddTimer([&event_loop, &ticks, |
| 208 | &application, pid]() { |
| 209 | ticks++; |
| 210 | if (application && application->status() == aos::starter::State::RUNNING) { |
| 211 | // Kill the application, it will autorestart. |
| 212 | kill(pid, SIGTERM); |
| 213 | application.reset(); |
| 214 | } |
| 215 | |
| 216 | // event loop lives for longer. |
| 217 | if (ticks >= 5) { |
| 218 | // Now we exit. |
| 219 | event_loop.Exit(); |
| 220 | } |
| 221 | }); |
| 222 | |
| 223 | event_loop.OnRun([&event_loop, exit_timer]() { |
| 224 | exit_timer->Schedule(event_loop.monotonic_now(), |
| 225 | std::chrono::milliseconds(1000)); |
| 226 | }); |
| 227 | |
| 228 | event_loop.Run(); |
| 229 | } |
James Kuszmaul | 37a56af | 2023-07-29 15:15:16 -0700 | [diff] [blame^] | 230 | |
| 231 | // Test that if the binary changes out from under us that we note it in the |
| 232 | // FileState. |
| 233 | TEST_F(SubprocessTest, ChangeBinaryContents) { |
| 234 | const std::string config_file = |
| 235 | ::aos::testing::ArtifactPath("aos/events/pingpong_config.json"); |
| 236 | |
| 237 | aos::FlatbufferDetachedBuffer<aos::Configuration> config = |
| 238 | aos::configuration::ReadConfig(config_file); |
| 239 | aos::ShmEventLoop event_loop(&config.message()); |
| 240 | |
| 241 | // Create a local copy of the sleep binary so that we can delete it. |
| 242 | const std::filesystem::path full_executable_path = |
| 243 | absl::StrCat(aos::testing::TestTmpDir(), "/", "sleep_binary"); |
| 244 | aos::util::WriteStringToFileOrDie( |
| 245 | full_executable_path.native(), |
| 246 | aos::util::ReadFileToStringOrDie(ResolvePath("sleep").native()), S_IRWXU); |
| 247 | |
| 248 | const std::filesystem::path executable_name = |
| 249 | absl::StrCat(aos::testing::TestTmpDir(), "/", "sleep_symlink"); |
| 250 | // Create a symlink that points to the actual binary, and test that a |
| 251 | // Creating a symlink in particular lets us ensure that our logic actually |
| 252 | // pays attention to the target file that we are running rather than the |
| 253 | // symlink itself (it also saves us having to cp a binary somewhere where we |
| 254 | // can overwrite it). |
| 255 | std::filesystem::create_symlink(full_executable_path.native(), |
| 256 | executable_name); |
| 257 | |
| 258 | // Wait until we are running, go through and test that various variations in |
| 259 | // file state result in the expected behavior, and then exit. |
| 260 | Application sleep( |
| 261 | "sleep", executable_name.native(), &event_loop, |
| 262 | [&sleep, &event_loop, executable_name, full_executable_path]() { |
| 263 | switch (sleep.status()) { |
| 264 | case aos::starter::State::RUNNING: |
| 265 | EXPECT_EQ(aos::starter::FileState::NO_CHANGE, |
| 266 | sleep.UpdateFileState()); |
| 267 | // Delete the symlink; this should have no effect, because the |
| 268 | // Application class should be looking at the original path. |
| 269 | std::filesystem::remove(executable_name); |
| 270 | EXPECT_EQ(aos::starter::FileState::NO_CHANGE, |
| 271 | sleep.UpdateFileState()); |
| 272 | // Delete the executable; it should be changed. |
| 273 | std::filesystem::remove(full_executable_path); |
| 274 | EXPECT_EQ(aos::starter::FileState::CHANGED, |
| 275 | sleep.UpdateFileState()); |
| 276 | // Replace the executable itself; it should be changed. |
| 277 | aos::util::WriteStringToFileOrDie(full_executable_path.native(), |
| 278 | "abcdef"); |
| 279 | EXPECT_EQ(aos::starter::FileState::CHANGED, |
| 280 | sleep.UpdateFileState()); |
| 281 | // Terminate. |
| 282 | event_loop.Exit(); |
| 283 | break; |
| 284 | case aos::starter::State::WAITING: |
| 285 | case aos::starter::State::STARTING: |
| 286 | case aos::starter::State::STOPPING: |
| 287 | case aos::starter::State::STOPPED: |
| 288 | EXPECT_EQ(aos::starter::FileState::NOT_RUNNING, |
| 289 | sleep.UpdateFileState()); |
| 290 | break; |
| 291 | } |
| 292 | }); |
| 293 | ASSERT_FALSE(sleep.autorestart()); |
| 294 | // Ensure that the subprocess will run longer than we care about (we just call |
| 295 | // Terminate() below to stop it). |
| 296 | sleep.set_args({"1000"}); |
| 297 | |
| 298 | sleep.Start(); |
| 299 | aos::TimerHandler *exit_timer = event_loop.AddTimer([&event_loop]() { |
| 300 | event_loop.Exit(); |
| 301 | FAIL() << "We should have already exited."; |
| 302 | }); |
| 303 | event_loop.OnRun([&event_loop, exit_timer]() { |
| 304 | exit_timer->Schedule(event_loop.monotonic_now() + |
| 305 | std::chrono::milliseconds(5000)); |
| 306 | }); |
| 307 | |
| 308 | event_loop.Run(); |
| 309 | sleep.Terminate(); |
| 310 | } |
| 311 | |
| 312 | class ResolvePathTest : public ::testing::Test { |
| 313 | protected: |
| 314 | ResolvePathTest() { |
| 315 | // Before doing anything else, |
| 316 | if (getenv("PATH") != nullptr) { |
| 317 | original_path_ = getenv("PATH"); |
| 318 | PCHECK(0 == unsetenv("PATH")); |
| 319 | } |
| 320 | } |
| 321 | |
| 322 | ~ResolvePathTest() { |
| 323 | if (!original_path_.empty()) { |
| 324 | PCHECK(0 == setenv("PATH", original_path_.c_str(), /*overwrite=*/1)); |
| 325 | } else { |
| 326 | PCHECK(0 == unsetenv("PATH")); |
| 327 | } |
| 328 | } |
| 329 | |
| 330 | std::filesystem::path GetLocalPath(const std::string filename) { |
| 331 | return absl::StrCat(aos::testing::TestTmpDir(), "/", filename); |
| 332 | } |
| 333 | |
| 334 | std::filesystem::path CreateFile(const std::string filename) { |
| 335 | const std::filesystem::path file = GetLocalPath(filename); |
| 336 | VLOG(2) << "Creating file at " << file; |
| 337 | util::WriteStringToFileOrDie(file.native(), "contents"); |
| 338 | return file; |
| 339 | } |
| 340 | |
| 341 | void SetPath(const std::vector<std::string> &path) { |
| 342 | PCHECK(0 == |
| 343 | setenv("PATH", absl::StrJoin(path, ":").c_str(), /*overwrite=*/1)); |
| 344 | } |
| 345 | |
| 346 | // Keep track of original PATH environment variable so that we can restore |
| 347 | // it. |
| 348 | std::string original_path_; |
| 349 | }; |
| 350 | |
| 351 | // Tests that we can resolve paths when there is no PATH environment variable. |
| 352 | TEST_F(ResolvePathTest, ResolveWithUnsetPath) { |
| 353 | const std::filesystem::path local_echo = CreateFile("echo"); |
| 354 | // Because the default path will be in /bin and /usr/bin (typically), we have |
| 355 | // to choose some utility that we can reasonably expect to be available in the |
| 356 | // test environment. |
| 357 | const std::filesystem::path echo_path = ResolvePath("echo"); |
| 358 | EXPECT_THAT((std::vector<std::string>{"/bin/echo", "/usr/bin/echo"}), |
| 359 | ::testing::Contains(echo_path.native())); |
| 360 | |
| 361 | // Test that a file with /'s in the name ignores the PATH. |
| 362 | const std::filesystem::path local_echo_path = |
| 363 | ResolvePath(local_echo.native()); |
| 364 | EXPECT_EQ(local_echo_path, local_echo); |
| 365 | } |
| 366 | |
| 367 | // Test that when the PATH environment variable is set that we can use it. |
| 368 | TEST_F(ResolvePathTest, ResolveWithPath) { |
| 369 | const std::filesystem::path local_folder = GetLocalPath("bin/"); |
| 370 | const std::filesystem::path local_folder2 = GetLocalPath("bin2/"); |
| 371 | aos::util::MkdirP(local_folder.native(), S_IRWXU); |
| 372 | aos::util::MkdirP(local_folder2.native(), S_IRWXU); |
| 373 | SetPath({local_folder, local_folder2}); |
| 374 | const std::filesystem::path binary = CreateFile("bin/binary"); |
| 375 | const std::filesystem::path duplicate_binary = CreateFile("bin2/binary"); |
| 376 | const std::filesystem::path other_name = CreateFile("bin2/other_name"); |
| 377 | |
| 378 | EXPECT_EQ(binary, ResolvePath("binary")); |
| 379 | EXPECT_EQ(other_name, ResolvePath("other_name")); |
| 380 | // And check that if we specify the full path for the duplicate binary that we |
| 381 | // can find it. |
| 382 | EXPECT_EQ(duplicate_binary, ResolvePath(duplicate_binary.native())); |
| 383 | } |
| 384 | |
| 385 | // Test that we fail to find non-existent files. |
| 386 | TEST_F(ResolvePathTest, DieOnFakeFile) { |
| 387 | // Fail to find something that searches the PATH. |
| 388 | SetPath({"foo", "bar"}); |
| 389 | EXPECT_DEATH(ResolvePath("fake_file"), "Unable to resolve"); |
| 390 | |
| 391 | // Fail to find a local file. |
| 392 | EXPECT_DEATH(ResolvePath("./fake_file"), "./fake_file does not exist"); |
| 393 | } |
| 394 | |
James Kuszmaul | d42edb4 | 2022-01-07 18:00:16 -0800 | [diff] [blame] | 395 | } // namespace aos::starter::testing |