diff options
author | Colin Cross <ccross@android.com> | 2023-10-05 15:42:58 -0700 |
---|---|---|
committer | Colin Cross <ccross@android.com> | 2023-10-13 20:34:54 +0000 |
commit | cf9e164fb5379456137fe9ba1263255b38051243 (patch) | |
tree | 08e1db01bcfbca123b7e1757d37134fa15964cf2 /toybox-gtests.cpp | |
parent | 732847b241f3568a89dc212d600fe558068510fa (diff) | |
download | toybox-cf9e164fb5379456137fe9ba1263255b38051243.tar.gz |
Wrap the toybox test scripts in a gtest binary
The test infrastructure understands the output from gtests, but can
only handle a single pass/fail status from an sh_test. Add a cc_test
that dynamically registers each tests/*.test file as a gtest that then
execs the toybox test shell scripts.
Test: atest toybox-gtests
Change-Id: I00de1bd3dd48724998866bcd17fe05597f351b50
Diffstat (limited to 'toybox-gtests.cpp')
-rw-r--r-- | toybox-gtests.cpp | 198 |
1 files changed, 198 insertions, 0 deletions
diff --git a/toybox-gtests.cpp b/toybox-gtests.cpp new file mode 100644 index 00000000..de7265c3 --- /dev/null +++ b/toybox-gtests.cpp @@ -0,0 +1,198 @@ +/* toybox-gtests.cpp - Wrapper around scripts/runtest.sh to run each toy test as a gtest + * + * Copyright 2023 The Android Open Source Project + */ + +#include <dirent.h> +#include <signal.h> +#include <stdlib.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <unistd.h> + +#include <algorithm> +#include <iostream> +#include <functional> +#include <memory> +#include <stdlib.h> +#include <string> +#include <vector> + +#include <gtest/gtest.h> + +#include <android-base/file.h> +#include <android-base/stringprintf.h> +#include <android-base/strings.h> +#include <android-base/test_utils.h> + +const std::string kShell = "/bin/bash"; + +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()); +} + +// RunTest 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 RunTest(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<const char*> 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<char**>(args.data())); + FAIL() << "Failed to exec " << kShell << " -c '" << test_cmd << "'"; +} + +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<std::string> 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 + RunTest(toy_, toy_path, test_file_, temp_dir.path); + } +} + +__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, decltype(&closedir)> dir(opendir(test_dir.c_str()), closedir); + if (!dir) { + std::cerr << "Cannot open test executable directory " << test_dir; + exit(1); + } + + std::vector<std::string> 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); + }); + } +} |