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