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