1 /* toybox-gtests.cpp - Wrapper around scripts/runtest.sh to run each toy test as a gtest
2 *
3 * Copyright 2023 The Android Open Source Project
4 */
5
6 #include <dirent.h>
7 #include <paths.h>
8 #include <signal.h>
9 #include <stdlib.h>
10 #include <sys/stat.h>
11 #include <sys/types.h>
12 #include <unistd.h>
13
14 #include <algorithm>
15 #include <iostream>
16 #include <functional>
17 #include <memory>
18 #include <stdlib.h>
19 #include <string>
20 #include <vector>
21
22 #include <gtest/gtest.h>
23
24 #include <android-base/file.h>
25 #include <android-base/stringprintf.h>
26 #include <android-base/strings.h>
27 #include <android-base/test_utils.h>
28
29 const std::string kShell =
30 #ifdef __ANDROID__
31 _PATH_BSHELL;
32 #else
33 // /bin/sh doesn't work when running on the host, the tests require /bin/bash
34 "/bin/bash";
35 #endif
36
MkdirOrFatal(std::string dir)37 static void MkdirOrFatal(std::string dir) {
38 int ret = mkdir(dir.c_str(), 0777);
39 ASSERT_EQ(ret, 0) << "Failed to make directory " << dir << ": " << strerror(errno);
40 }
41
SystemStdoutOrFatal(std::string cmd)42 static std::string SystemStdoutOrFatal(std::string cmd) {
43 CapturedStdout stdout_str;
44 int ret = system(cmd.c_str());
45 stdout_str.Stop();
46 EXPECT_GE(ret, 0) << "Failed to run " << cmd << ": " << strerror(errno);
47 EXPECT_EQ(ret, 0) << "Failed to run " << cmd << ": exited with status " << ret;
48 return android::base::Trim(stdout_str.str());
49 }
50
51 // ExecTest sets up the environemnt and then execs the toybox test scripts for a single toy.
52 // It is run in a subprocess as a gtest death test.
ExecTest(std::string toy,std::string toy_path,std::string test_file,std::string temp_dir)53 static void ExecTest(std::string toy,
54 std::string toy_path,
55 std::string test_file,
56 std::string temp_dir) {
57 std::string test_binary_dir = android::base::GetExecutableDirectory();
58
59 std::string working_dir = temp_dir + "/" + toy;
60 MkdirOrFatal(working_dir);
61
62 #ifndef __ANDROID__
63 std::string path_env = getenv("PATH");
64 path_env = temp_dir + "/path:" + path_env;
65 setenv("PATH", path_env.c_str(), true);
66 #endif
67
68 setenv("C", toy_path.c_str(), true);
69 setenv("CMDNAME", toy.c_str(), true);
70 setenv("TESTDIR", temp_dir.c_str(), true);
71 setenv("FILES", (test_binary_dir + "/tests/files").c_str(), true);
72 setenv("LANG", "en_US.UTF-8", true);
73 setenv("VERBOSE", "1", true);
74
75 std::string test_cmd = android::base::StringPrintf(
76 "cd %s && source %s/scripts/runtest.sh && source %s/tests/%s",
77 working_dir.c_str(),
78 test_binary_dir.c_str(),
79 test_binary_dir.c_str(),
80 test_file.c_str());
81
82 std::vector<const char*> args;
83 args.push_back(kShell.c_str());
84 args.push_back("-c");
85 args.push_back(test_cmd.c_str());
86 args.push_back(NULL);
87
88 // When running in atest something is configure the SIGQUIT signal as blocked, which
89 // causes some missed signals in toybox tests that leave dangling "sleep 100" processes
90 // lying around. These processes have the gtest pipe fd open, and keep gtest from
91 // considering the death test to have exited until the sleep ends.
92 sigset_t signal_set;
93 sigemptyset(&signal_set);
94 sigaddset(&signal_set, SIGQUIT);
95 sigprocmask(SIG_UNBLOCK, &signal_set, nullptr);
96
97 execv(args[0], const_cast<char**>(args.data()));
98 FAIL() << "Failed to exec " << kShell << " -c '" << test_cmd << "'" << strerror(errno);
99 }
100
101 class ToyboxTest : public testing::Test {
102 public:
ToyboxTest(std::string toy,std::string test_file)103 ToyboxTest(std::string toy, std::string test_file) : toy_(toy), test_file_(test_file) { }
104 void TestBody();
105 private:
106 std::string toy_;
107 std::string test_file_;
108 };
109
110
TestBody()111 void ToyboxTest::TestBody() {
112 // This test function is run once for each toy.
113 TemporaryDir temp_dir{};
114 bool ignore_failures = false;
115
116 #ifdef __ANDROID__
117 // On the device, check whether the toy exists
118 std::string toy_path = SystemStdoutOrFatal(std::string("which ") + toy_ + " || true");
119 if (toy_path.empty()) {
120 GTEST_SKIP() << toy_ << " not present";
121 }
122
123 // And whether it is uses toybox as its implementation.
124 std::string implementation = SystemStdoutOrFatal(std::string("realpath ") + toy_path);
125 if (!android::base::EndsWith(implementation, "/toybox")) {
126 std::cout << toy_ << " is *not* toybox; this does not count as a test failure";
127 // If there is no symlink for the toy on the device then run the tests but don't report
128 // failures.
129 ignore_failures = true;
130 }
131 #else
132 // On the host toybox is packaged with the test so that it can be run in CI, which won't
133 // have access to the prebuilt toybox or path symlinks. It is packaged without any toy
134 // symlinks, so a symlink is created for each test.
135
136 // Test if the toy is supported by the packaged toybox, and skip the test if not.
137 std::string toybox_path = android::base::GetExecutableDirectory() + "/toybox";
138 std::string supported_toys_str = SystemStdoutOrFatal(toybox_path);
139 std::vector<std::string> supported_toys = android::base::Split(supported_toys_str, " \n");
140 if (std::find(supported_toys.begin(), supported_toys.end(), toy_) == supported_toys.end()) {
141 GTEST_SKIP() << toy_ << " not compiled into toybox";
142 }
143
144 // Create a directory with a symlinks for all the toys that will be prepended to PATH.
145 // Some tests have interdependencies on other toys that may not be available in
146 // the host system, e.g. the tar tests depend on the file tool.
147 std::string path_dir = std::string(temp_dir.path) + "/path";
148 MkdirOrFatal(path_dir);
149 for (auto& toy : supported_toys) {
150 std::string toy_path = path_dir + "/" + toy;
151 int ret = symlink(toybox_path.c_str(), toy_path.c_str());
152 ASSERT_EQ(ret, 0) <<
153 "Failed to symlink " << toy_path << " to " << toybox_path << ": " << strerror(errno);
154 }
155 std::string toy_path = path_dir + "/" + toy_;
156 #endif
157
158 pid_t pid = fork();
159 ASSERT_GE(pid, 0) << "Failed to fork";
160 if (pid > 0) {
161 // parent
162 int status = 0;
163 int ret = waitpid(pid, &status, 0);
164 ASSERT_GT(pid, 0) << "Failed to wait for child " << pid << ": " << strerror(errno);
165 ASSERT_TRUE(WIFEXITED(status));
166 if (!ignore_failures) {
167 int exit_status = WEXITSTATUS(status);
168 ASSERT_EQ(exit_status, 0);
169 }
170 } else {
171 // child
172 ExecTest(toy_, toy_path, test_file_, temp_dir.path);
173 _exit(1);
174 }
175 }
176
initTests()177 __attribute__((constructor)) static void initTests() {
178 // Find all the "tests/*.test" files packaged alongside the gtest.
179 std::string test_dir = android::base::GetExecutableDirectory() + "/tests";
180 std::unique_ptr<DIR, decltype(&closedir)> dir(opendir(test_dir.c_str()), closedir);
181 if (!dir) {
182 std::cerr << "Cannot open test executable directory " << test_dir;
183 exit(1);
184 }
185
186 std::vector<std::string> test_files;
187 dirent* de;
188 while ((de = readdir(dir.get())) != nullptr) {
189 std::string file = de->d_name;
190 if (android::base::EndsWith(file, ".test")) {
191 test_files.push_back(file);
192 }
193 }
194
195 std::sort(test_files.begin(), test_files.end());
196
197 // Register each test file as an individual gtest.
198 for (auto& test_file : test_files) {
199 std::string toy = test_file.substr(0, test_file.size() - strlen(".test"));
200
201 testing::RegisterTest("toybox", toy.c_str(), nullptr, nullptr, __FILE__, __LINE__,
202 [=]() -> ToyboxTest* {
203 return new ToyboxTest(toy, test_file);
204 });
205 }
206 }
207