/* toybox-gtests.cpp - Wrapper around scripts/runtest.sh to run each toy test as a gtest * * Copyright 2023 The Android Open Source Project */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include const std::string kShell = #ifdef __ANDROID__ _PATH_BSHELL; #else // /bin/sh doesn't work when running on the host, the tests require /bin/bash "/bin/bash"; #endif static void MkdirOrFatal(std::string dir) { int ret = mkdir(dir.c_str(), 0777); ASSERT_EQ(ret, 0) << "Failed to make directory " << dir << ": " << strerror(errno); } static std::string SystemStdoutOrFatal(std::string cmd) { CapturedStdout stdout_str; int ret = system(cmd.c_str()); stdout_str.Stop(); EXPECT_GE(ret, 0) << "Failed to run " << cmd << ": " << strerror(errno); EXPECT_EQ(ret, 0) << "Failed to run " << cmd << ": exited with status " << ret; return android::base::Trim(stdout_str.str()); } // ExecTest sets up the environemnt and then execs the toybox test scripts for a single toy. // It is run in a subprocess as a gtest death test. static void ExecTest(std::string toy, std::string toy_path, std::string test_file, std::string temp_dir) { std::string test_binary_dir = android::base::GetExecutableDirectory(); std::string working_dir = temp_dir + "/" + toy; MkdirOrFatal(working_dir); #ifndef __ANDROID__ std::string path_env = getenv("PATH"); path_env = temp_dir + "/path:" + path_env; setenv("PATH", path_env.c_str(), true); #endif setenv("C", toy_path.c_str(), true); setenv("CMDNAME", toy.c_str(), true); setenv("TESTDIR", temp_dir.c_str(), true); setenv("FILES", (test_binary_dir + "/tests/files").c_str(), true); setenv("LANG", "en_US.UTF-8", true); setenv("VERBOSE", "1", true); std::string test_cmd = android::base::StringPrintf( "cd %s && source %s/scripts/runtest.sh && source %s/tests/%s", working_dir.c_str(), test_binary_dir.c_str(), test_binary_dir.c_str(), test_file.c_str()); std::vector args; args.push_back(kShell.c_str()); args.push_back("-c"); args.push_back(test_cmd.c_str()); args.push_back(NULL); // When running in atest something is configure the SIGQUIT signal as blocked, which // causes some missed signals in toybox tests that leave dangling "sleep 100" processes // lying around. These processes have the gtest pipe fd open, and keep gtest from // considering the death test to have exited until the sleep ends. sigset_t signal_set; sigemptyset(&signal_set); sigaddset(&signal_set, SIGQUIT); sigprocmask(SIG_UNBLOCK, &signal_set, nullptr); execv(args[0], const_cast(args.data())); FAIL() << "Failed to exec " << kShell << " -c '" << test_cmd << "'" << strerror(errno); } class ToyboxTest : public testing::Test { public: ToyboxTest(std::string toy, std::string test_file) : toy_(toy), test_file_(test_file) { } void TestBody(); private: std::string toy_; std::string test_file_; }; void ToyboxTest::TestBody() { // This test function is run once for each toy. TemporaryDir temp_dir{}; bool ignore_failures = false; #ifdef __ANDROID__ // On the device, check whether the toy exists std::string toy_path = SystemStdoutOrFatal(std::string("which ") + toy_ + " || true"); if (toy_path.empty()) { GTEST_SKIP() << toy_ << " not present"; } // And whether it is uses toybox as its implementation. std::string implementation = SystemStdoutOrFatal(std::string("realpath ") + toy_path); if (!android::base::EndsWith(implementation, "/toybox")) { std::cout << toy_ << " is non-toybox implementation"; // If there is no symlink for the toy on the device then run the tests but don't report // failures. ignore_failures = true; } #else // On the host toybox is packaged with the test so that it can be run in CI, which won't // have access to the prebuilt toybox or path symlinks. It is packaged without any toy // symlinks, so a symlink is created for each test. // Test if the toy is supported by the packaged toybox, and skip the test if not. std::string toybox_path = android::base::GetExecutableDirectory() + "/toybox"; std::string supported_toys_str = SystemStdoutOrFatal(toybox_path); std::vector supported_toys = android::base::Split(supported_toys_str, " \n"); if (std::find(supported_toys.begin(), supported_toys.end(), toy_) == supported_toys.end()) { GTEST_SKIP() << toy_ << " not compiled into toybox"; } // Create a directory with a symlinks for all the toys that will be prepended to PATH. // Some tests have interdependencies on other toys that may not be available in // the host system, e.g. the tar tests depend on the file tool. std::string path_dir = std::string(temp_dir.path) + "/path"; MkdirOrFatal(path_dir); for (auto& toy : supported_toys) { std::string toy_path = path_dir + "/" + toy; int ret = symlink(toybox_path.c_str(), toy_path.c_str()); ASSERT_EQ(ret, 0) << "Failed to symlink " << toy_path << " to " << toybox_path << ": " << strerror(errno); } std::string toy_path = path_dir + "/" + toy_; #endif pid_t pid = fork(); ASSERT_GE(pid, 0) << "Failed to fork"; if (pid > 0) { // parent int status = 0; int ret = waitpid(pid, &status, 0); ASSERT_GT(pid, 0) << "Failed to wait for child " << pid << ": " << strerror(errno); ASSERT_TRUE(WIFEXITED(status)); if (!ignore_failures) { int exit_status = WEXITSTATUS(status); ASSERT_EQ(exit_status, 0); } } else { // child ExecTest(toy_, toy_path, test_file_, temp_dir.path); _exit(1); } } __attribute__((constructor)) static void initTests() { // Find all the "tests/*.test" files packaged alongside the gtest. std::string test_dir = android::base::GetExecutableDirectory() + "/tests"; std::unique_ptr dir(opendir(test_dir.c_str()), closedir); if (!dir) { std::cerr << "Cannot open test executable directory " << test_dir; exit(1); } std::vector test_files; dirent* de; while ((de = readdir(dir.get())) != nullptr) { std::string file = de->d_name; if (android::base::EndsWith(file, ".test")) { test_files.push_back(file); } } std::sort(test_files.begin(), test_files.end()); // Register each test file as an individual gtest. for (auto& test_file : test_files) { std::string toy = test_file.substr(0, test_file.size() - strlen(".test")); testing::RegisterTest("toybox", toy.c_str(), nullptr, nullptr, __FILE__, __LINE__, [=]() -> ToyboxTest* { return new ToyboxTest(toy, test_file); }); } }