blob: 12f7f6d89e651f3265cbc9ea3cefd473cb03c277 [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
payton.rehl2841b1c2023-05-25 17:23:55 -0700108TEST_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
150TEST_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 Snaider70deaf22023-08-11 13:58:34 -0700187// 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.
192TEST_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 Kuszmaul37a56af2023-07-29 15:15:16 -0700230
231// Test that if the binary changes out from under us that we note it in the
232// FileState.
233TEST_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
312class 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.
352TEST_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.
368TEST_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.
386TEST_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 Kuszmauld42edb42022-01-07 18:00:16 -0800395} // namespace aos::starter::testing