summaryrefslogtreecommitdiff
path: root/libarttools/tools
diff options
context:
space:
mode:
Diffstat (limited to 'libarttools/tools')
-rw-r--r--libarttools/tools/art_exec.cc225
-rw-r--r--libarttools/tools/art_exec_test.cc262
-rw-r--r--libarttools/tools/cmdline_builder.h156
-rw-r--r--libarttools/tools/cmdline_builder_test.cc135
-rw-r--r--libarttools/tools/system_properties.h104
-rw-r--r--libarttools/tools/system_properties_test.cc97
-rw-r--r--libarttools/tools/tools.cc121
-rw-r--r--libarttools/tools/tools.h15
-rw-r--r--libarttools/tools/tools_test.cc93
9 files changed, 1202 insertions, 6 deletions
diff --git a/libarttools/tools/art_exec.cc b/libarttools/tools/art_exec.cc
new file mode 100644
index 0000000000..1806ed4514
--- /dev/null
+++ b/libarttools/tools/art_exec.cc
@@ -0,0 +1,225 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <stdlib.h>
+#include <sys/capability.h>
+#include <sys/resource.h>
+#include <unistd.h>
+
+#include <filesystem>
+#include <iostream>
+#include <iterator>
+#include <optional>
+#include <string>
+#include <string_view>
+#include <system_error>
+#include <unordered_map>
+#include <unordered_set>
+#include <vector>
+
+#include "android-base/logging.h"
+#include "android-base/parseint.h"
+#include "android-base/result.h"
+#include "android-base/strings.h"
+#include "base/macros.h"
+#include "base/scoped_cap.h"
+#include "fmt/format.h"
+#include "palette/palette.h"
+#include "system/thread_defs.h"
+
+namespace {
+
+using ::android::base::ConsumePrefix;
+using ::android::base::Join;
+using ::android::base::ParseInt;
+using ::android::base::Result;
+using ::android::base::Split;
+
+using ::fmt::literals::operator""_format; // NOLINT
+
+constexpr const char* kUsage =
+ R"(A wrapper binary that configures the process and executes a command.
+
+By default, it closes all open file descriptors except stdin, stdout, and stderr. `--keep-fds` can
+be passed to keep some more file descriptors open.
+
+Usage: art_exec [OPTIONS]... -- [COMMAND]...
+
+Supported options:
+ --help: Print this text.
+ --set-task-profile=PROFILES: Apply a set of task profiles (see
+ https://source.android.com/devices/tech/perf/cgroups). Requires root access. PROFILES can be a
+ comma-separated list of task profile names.
+ --set-priority=PRIORITY: Apply the process priority. Currently, the only supported value of
+ PRIORITY is "background".
+ --drop-capabilities: Drop all root capabilities. Note that this has effect only if `art_exec` runs
+ with some root capabilities but not as the root user.
+ --keep-fds=FILE_DESCRIPTORS: A semicolon-separated list of file descriptors to keep open.
+ --env=KEY=VALUE: Set an environment variable. This flag can be passed multiple times to set
+ multiple environment variables.
+)";
+
+constexpr int kErrorUsage = 100;
+constexpr int kErrorOther = 101;
+
+struct Options {
+ int command_pos = -1;
+ std::vector<std::string> task_profiles;
+ std::optional<int> priority = std::nullopt;
+ bool drop_capabilities = false;
+ std::unordered_set<int> keep_fds{fileno(stdin), fileno(stdout), fileno(stderr)};
+ std::unordered_map<std::string, std::string> envs;
+};
+
+[[noreturn]] void Usage(const std::string& error_msg) {
+ LOG(ERROR) << error_msg;
+ std::cerr << error_msg << "\n" << kUsage << "\n";
+ exit(kErrorUsage);
+}
+
+Options ParseOptions(int argc, char** argv) {
+ Options options;
+ for (int i = 1; i < argc; i++) {
+ std::string_view arg = argv[i];
+ if (arg == "--help") {
+ std::cerr << kUsage << "\n";
+ exit(0);
+ } else if (ConsumePrefix(&arg, "--set-task-profile=")) {
+ options.task_profiles = Split(std::string(arg), ",");
+ if (options.task_profiles.empty()) {
+ Usage("Empty task profile list");
+ }
+ } else if (ConsumePrefix(&arg, "--set-priority=")) {
+ if (arg == "background") {
+ options.priority = ANDROID_PRIORITY_BACKGROUND;
+ } else {
+ Usage("Unknown priority " + std::string(arg));
+ }
+ } else if (arg == "--drop-capabilities") {
+ options.drop_capabilities = true;
+ } else if (ConsumePrefix(&arg, "--keep-fds=")) {
+ for (const std::string& fd_str : Split(std::string(arg), ":")) {
+ int fd;
+ if (!ParseInt(fd_str, &fd)) {
+ Usage("Invalid fd " + fd_str);
+ }
+ options.keep_fds.insert(fd);
+ }
+ } else if (ConsumePrefix(&arg, "--env=")) {
+ size_t pos = arg.find('=');
+ if (pos == std::string_view::npos) {
+ Usage("Malformed environment variable. Must contain '='");
+ }
+ options.envs[std::string(arg.substr(/*pos=*/0, /*n=*/pos))] =
+ std::string(arg.substr(pos + 1));
+ } else if (arg == "--") {
+ if (i + 1 >= argc) {
+ Usage("Missing command after '--'");
+ }
+ options.command_pos = i + 1;
+ return options;
+ } else {
+ Usage("Unknown option " + std::string(arg));
+ }
+ }
+ Usage("Missing '--'");
+}
+
+Result<void> DropInheritableCaps() {
+ art::ScopedCap cap(cap_get_proc());
+ if (cap.Get() == nullptr) {
+ return ErrnoErrorf("Failed to call cap_get_proc");
+ }
+ if (cap_clear_flag(cap.Get(), CAP_INHERITABLE) != 0) {
+ return ErrnoErrorf("Failed to call cap_clear_flag");
+ }
+ if (cap_set_proc(cap.Get()) != 0) {
+ return ErrnoErrorf("Failed to call cap_set_proc");
+ }
+ return {};
+}
+
+Result<void> CloseFds(const std::unordered_set<int>& keep_fds) {
+ std::vector<int> open_fds;
+ std::error_code ec;
+ for (const std::filesystem::directory_entry& dir_entry :
+ std::filesystem::directory_iterator("/proc/self/fd", ec)) {
+ int fd;
+ if (!ParseInt(dir_entry.path().filename(), &fd)) {
+ return Errorf("Invalid entry in /proc/self/fd {}", dir_entry.path().filename());
+ }
+ open_fds.push_back(fd);
+ }
+ if (ec) {
+ return Errorf("Failed to list open FDs: {}", ec.message());
+ }
+ for (int fd : open_fds) {
+ if (keep_fds.find(fd) == keep_fds.end()) {
+ if (close(fd) != 0) {
+ Result<void> error = ErrnoErrorf("Failed to close FD {}", fd);
+ if (std::filesystem::exists("/proc/self/fd/{}"_format(fd))) {
+ return error;
+ }
+ }
+ }
+ }
+ return {};
+}
+
+} // namespace
+
+int main(int argc, char** argv) {
+ android::base::InitLogging(argv);
+
+ Options options = ParseOptions(argc, argv);
+
+ if (auto result = CloseFds(options.keep_fds); !result.ok()) {
+ LOG(ERROR) << "Failed to close open FDs: " << result.error();
+ return kErrorOther;
+ }
+
+ if (!options.task_profiles.empty()) {
+ if (int ret = PaletteSetTaskProfiles(/*tid=*/0, options.task_profiles);
+ ret != PALETTE_STATUS_OK) {
+ LOG(ERROR) << "Failed to set task profile: " << ret;
+ return kErrorOther;
+ }
+ }
+
+ if (options.priority.has_value()) {
+ if (setpriority(PRIO_PROCESS, /*who=*/0, options.priority.value()) != 0) {
+ PLOG(ERROR) << "Failed to setpriority";
+ return kErrorOther;
+ }
+ }
+
+ if (options.drop_capabilities) {
+ if (auto result = DropInheritableCaps(); !result.ok()) {
+ LOG(ERROR) << "Failed to drop inheritable capabilities: " << result.error();
+ return kErrorOther;
+ }
+ }
+
+ for (const auto& [key, value] : options.envs) {
+ setenv(key.c_str(), value.c_str(), /*overwrite=*/1);
+ }
+
+ execv(argv[options.command_pos], argv + options.command_pos);
+
+ std::vector<const char*> command_args(argv + options.command_pos, argv + argc);
+ PLOG(FATAL) << "Failed to execute (" << Join(command_args, ' ') << ")";
+ UNREACHABLE();
+}
diff --git a/libarttools/tools/art_exec_test.cc b/libarttools/tools/art_exec_test.cc
new file mode 100644
index 0000000000..9e8b0de3ad
--- /dev/null
+++ b/libarttools/tools/art_exec_test.cc
@@ -0,0 +1,262 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <sys/capability.h>
+#include <sys/resource.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include <csignal>
+#include <filesystem>
+#include <functional>
+#include <string>
+#include <utility>
+
+#include "android-base/file.h"
+#include "android-base/logging.h"
+#include "android-base/scopeguard.h"
+#include "android-base/strings.h"
+#include "base/common_art_test.h"
+#include "base/file_utils.h"
+#include "base/globals.h"
+#include "base/macros.h"
+#include "base/os.h"
+#include "base/scoped_cap.h"
+#include "exec_utils.h"
+#include "fmt/format.h"
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "system/thread_defs.h"
+
+#ifdef ART_TARGET_ANDROID
+#include "android-modules-utils/sdk_level.h"
+#endif
+
+namespace art {
+namespace {
+
+using ::android::base::make_scope_guard;
+using ::android::base::ScopeGuard;
+using ::android::base::Split;
+using ::testing::Contains;
+using ::testing::ElementsAre;
+using ::testing::HasSubstr;
+using ::testing::Not;
+
+// clang-tidy incorrectly complaints about the using declaration while the user-defined literal is
+// actually being used.
+using ::fmt::literals::operator""_format; // NOLINT
+
+constexpr uid_t kRoot = 0;
+constexpr uid_t kNobody = 9999;
+
+// This test executes a few Linux system commands such as "ls", which are linked against system
+// libraries. In many ART gtests we set LD_LIBRARY_PATH to make the test binaries link to libraries
+// from the ART module first, and if that setting is propagated to the system commands they may also
+// try to link to those libraries instead of the system ones they are built against. This is
+// particularly noticeable when 32-bit tests run on a 64-bit system. Hence we need to set
+// LD_LIBRARY_PATH to an empty string here.
+// TODO(b/247108425): Remove this when ART gtests no longer use LD_LIBRARY_PATH.
+constexpr const char* kEmptyLdLibraryPath = "--env=LD_LIBRARY_PATH=";
+
+std::string GetArtBin(const std::string& name) { return "{}/bin/{}"_format(GetArtRoot(), name); }
+
+std::string GetBin(const std::string& name) { return "{}/bin/{}"_format(GetAndroidRoot(), name); }
+
+// Executes the command, waits for it to finish, and keeps it in a waitable state until the current
+// scope exits.
+std::pair<pid_t, ScopeGuard<std::function<void()>>> ScopedExecAndWait(
+ std::vector<std::string>& args) {
+ std::vector<char*> execv_args;
+ execv_args.reserve(args.size() + 1);
+ for (std::string& arg : args) {
+ execv_args.push_back(arg.data());
+ }
+ execv_args.push_back(nullptr);
+
+ pid_t pid = fork();
+ if (pid == 0) {
+ execv(execv_args[0], execv_args.data());
+ UNREACHABLE();
+ } else if (pid > 0) {
+ siginfo_t info;
+ CHECK_EQ(TEMP_FAILURE_RETRY(waitid(P_PID, pid, &info, WEXITED | WNOWAIT)), 0);
+ CHECK_EQ(info.si_code, CLD_EXITED);
+ CHECK_EQ(info.si_status, 0);
+ std::function<void()> cleanup([=] {
+ siginfo_t info;
+ CHECK_EQ(TEMP_FAILURE_RETRY(waitid(P_PID, pid, &info, WEXITED)), 0);
+ });
+ return std::make_pair(pid, make_scope_guard(std::move(cleanup)));
+ } else {
+ LOG(FATAL) << "Failed to call fork";
+ UNREACHABLE();
+ }
+}
+
+// Grants the current process the given root capability.
+void SetCap(cap_flag_t flag, cap_value_t value) {
+ ScopedCap cap(cap_get_proc());
+ CHECK_NE(cap.Get(), nullptr);
+ cap_value_t caps[]{value};
+ CHECK_EQ(cap_set_flag(cap.Get(), flag, /*ncap=*/1, caps, CAP_SET), 0);
+ CHECK_EQ(cap_set_proc(cap.Get()), 0);
+}
+
+// Returns true if the given process has the given root capability.
+bool GetCap(pid_t pid, cap_flag_t flag, cap_value_t value) {
+ ScopedCap cap(cap_get_pid(pid));
+ CHECK_NE(cap.Get(), nullptr);
+ cap_flag_value_t flag_value;
+ CHECK_EQ(cap_get_flag(cap.Get(), value, flag, &flag_value), 0);
+ return flag_value == CAP_SET;
+}
+
+class ArtExecTest : public testing::Test {
+ protected:
+ void SetUp() override {
+ testing::Test::SetUp();
+ if (!kIsTargetAndroid) {
+ GTEST_SKIP() << "art_exec is for device only";
+ }
+ if (getuid() != kRoot) {
+ GTEST_SKIP() << "art_exec requires root";
+ }
+ art_exec_bin_ = GetArtBin("art_exec");
+ }
+
+ std::string art_exec_bin_;
+};
+
+TEST_F(ArtExecTest, Command) {
+ std::string error_msg;
+ int ret = ExecAndReturnCode({art_exec_bin_, "--", GetBin("sh"), "-c", "exit 123"}, &error_msg);
+ ASSERT_EQ(ret, 123) << error_msg;
+}
+
+TEST_F(ArtExecTest, SetTaskProfiles) {
+// The condition is always true because ArtExecTest is run on device only.
+#ifdef ART_TARGET_ANDROID
+ if (!android::modules::sdklevel::IsAtLeastU()) {
+ GTEST_SKIP() << "This test depends on a libartpalette API that is only available on U+";
+ }
+#endif
+
+ std::string filename = "/data/local/tmp/art-exec-test-XXXXXX";
+ ScratchFile scratch_file(new File(mkstemp(filename.data()), filename, /*check_usage=*/false));
+ ASSERT_GE(scratch_file.GetFd(), 0);
+
+ std::vector<std::string> args{art_exec_bin_,
+ "--set-task-profile=ProcessCapacityHigh",
+ kEmptyLdLibraryPath,
+ "--",
+ GetBin("sh"),
+ "-c",
+ "cat /proc/self/cgroup > " + filename};
+ auto [pid, scope_guard] = ScopedExecAndWait(args);
+ std::string cgroup;
+ ASSERT_TRUE(android::base::ReadFileToString(filename, &cgroup));
+ EXPECT_THAT(cgroup, HasSubstr(":cpuset:/foreground\n"));
+}
+
+TEST_F(ArtExecTest, SetPriority) {
+ std::vector<std::string> args{
+ art_exec_bin_, "--set-priority=background", kEmptyLdLibraryPath, "--", GetBin("true")};
+ auto [pid, scope_guard] = ScopedExecAndWait(args);
+ EXPECT_EQ(getpriority(PRIO_PROCESS, pid), ANDROID_PRIORITY_BACKGROUND);
+}
+
+TEST_F(ArtExecTest, DropCapabilities) {
+ // Switch to a non-root user, but still keep the CAP_FOWNER capability available and inheritable.
+ // The order of the following calls matters.
+ CHECK_EQ(cap_setuid(kNobody), 0);
+ SetCap(CAP_INHERITABLE, CAP_FOWNER);
+ SetCap(CAP_EFFECTIVE, CAP_FOWNER);
+ ASSERT_EQ(cap_set_ambient(CAP_FOWNER, CAP_SET), 0);
+
+ // Make sure the test is set up correctly (i.e., the child process should normally have the
+ // inherited root capability: CAP_FOWNER).
+ {
+ std::vector<std::string> args{art_exec_bin_, kEmptyLdLibraryPath, "--", GetBin("true")};
+ auto [pid, scope_guard] = ScopedExecAndWait(args);
+ ASSERT_TRUE(GetCap(pid, CAP_EFFECTIVE, CAP_FOWNER));
+ }
+
+ {
+ std::vector<std::string> args{
+ art_exec_bin_, "--drop-capabilities", kEmptyLdLibraryPath, "--", GetBin("true")};
+ auto [pid, scope_guard] = ScopedExecAndWait(args);
+ EXPECT_FALSE(GetCap(pid, CAP_EFFECTIVE, CAP_FOWNER));
+ }
+}
+
+TEST_F(ArtExecTest, CloseFds) {
+ std::unique_ptr<File> file1(OS::OpenFileForReading("/dev/zero"));
+ std::unique_ptr<File> file2(OS::OpenFileForReading("/dev/zero"));
+ std::unique_ptr<File> file3(OS::OpenFileForReading("/dev/zero"));
+ ASSERT_NE(file1, nullptr);
+ ASSERT_NE(file2, nullptr);
+ ASSERT_NE(file3, nullptr);
+
+ std::string filename = "/data/local/tmp/art-exec-test-XXXXXX";
+ ScratchFile scratch_file(new File(mkstemp(filename.data()), filename, /*check_usage=*/false));
+ ASSERT_GE(scratch_file.GetFd(), 0);
+
+ std::vector<std::string> args{art_exec_bin_,
+ "--keep-fds={}:{}"_format(file3->Fd(), file2->Fd()),
+ kEmptyLdLibraryPath,
+ "--",
+ GetBin("sh"),
+ "-c",
+ "("
+ "readlink /proc/self/fd/{} || echo;"
+ "readlink /proc/self/fd/{} || echo;"
+ "readlink /proc/self/fd/{} || echo;"
+ ") > {}"_format(file1->Fd(), file2->Fd(), file3->Fd(), filename)};
+
+ ScopedExecAndWait(args);
+
+ std::string open_fds;
+ ASSERT_TRUE(android::base::ReadFileToString(filename, &open_fds));
+
+ // `file1` should be closed, while the other two should be open. There's a blank line at the end.
+ EXPECT_THAT(Split(open_fds, "\n"), ElementsAre(Not("/dev/zero"), "/dev/zero", "/dev/zero", ""));
+}
+
+TEST_F(ArtExecTest, Env) {
+ std::string filename = "/data/local/tmp/art-exec-test-XXXXXX";
+ ScratchFile scratch_file(new File(mkstemp(filename.data()), filename, /*check_usage=*/false));
+ ASSERT_GE(scratch_file.GetFd(), 0);
+
+ std::vector<std::string> args{art_exec_bin_,
+ "--env=FOO=BAR",
+ kEmptyLdLibraryPath,
+ "--",
+ GetBin("sh"),
+ "-c",
+ "env > " + filename};
+
+ ScopedExecAndWait(args);
+
+ std::string envs;
+ ASSERT_TRUE(android::base::ReadFileToString(filename, &envs));
+
+ EXPECT_THAT(Split(envs, "\n"), Contains("FOO=BAR"));
+}
+
+} // namespace
+} // namespace art
diff --git a/libarttools/tools/cmdline_builder.h b/libarttools/tools/cmdline_builder.h
new file mode 100644
index 0000000000..fd11ee87ba
--- /dev/null
+++ b/libarttools/tools/cmdline_builder.h
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef ART_LIBARTTOOLS_TOOLS_CMDLINE_BUILDER_H_
+#define ART_LIBARTTOOLS_TOOLS_CMDLINE_BUILDER_H_
+
+#include <algorithm>
+#include <iterator>
+#include <string>
+#include <string_view>
+#include <vector>
+
+#include "android-base/stringprintf.h"
+
+namespace art {
+namespace tools {
+
+namespace internal {
+
+constexpr bool ContainsOneFormatSpecifier(std::string_view format, char specifier) {
+ int count = 0;
+ size_t pos = 0;
+ while ((pos = format.find('%', pos)) != std::string_view::npos) {
+ if (pos == format.length() - 1) {
+ // Invalid trailing '%'.
+ return false;
+ }
+ if (format[pos + 1] == specifier) {
+ count++;
+ } else if (format[pos + 1] != '%') {
+ // "%%" is okay. Otherwise, it's a wrong specifier.
+ return false;
+ }
+ pos += 2;
+ }
+ return count == 1;
+}
+
+} // namespace internal
+
+// A util class that builds cmdline arguments.
+class CmdlineBuilder {
+ public:
+ // Returns all arguments.
+ const std::vector<std::string>& Get() const { return elements_; }
+
+ // Adds an argument as-is.
+ CmdlineBuilder& Add(std::string_view arg) {
+ elements_.push_back(std::string(arg));
+ return *this;
+ }
+
+ // Same as above but adds a runtime argument.
+ CmdlineBuilder& AddRuntime(std::string_view arg) { return Add("--runtime-arg").Add(arg); }
+
+ // Adds a string value formatted by the format string.
+ //
+ // Usage: Add("--flag=%s", "value")
+ CmdlineBuilder& Add(const char* arg_format, const std::string& value)
+ __attribute__((enable_if(internal::ContainsOneFormatSpecifier(arg_format, 's'),
+ "'arg' must be a string literal that contains '%s'"))) {
+ return Add(android::base::StringPrintf(arg_format, value.c_str()));
+ }
+
+ // Same as above but adds a runtime argument.
+ CmdlineBuilder& AddRuntime(const char* arg_format, const std::string& value)
+ __attribute__((enable_if(internal::ContainsOneFormatSpecifier(arg_format, 's'),
+ "'arg' must be a string literal that contains '%s'"))) {
+ return AddRuntime(android::base::StringPrintf(arg_format, value.c_str()));
+ }
+
+ // Adds an integer value formatted by the format string.
+ //
+ // Usage: Add("--flag=%d", 123)
+ CmdlineBuilder& Add(const char* arg_format, int value)
+ __attribute__((enable_if(internal::ContainsOneFormatSpecifier(arg_format, 'd'),
+ "'arg' must be a string literal that contains '%d'"))) {
+ return Add(android::base::StringPrintf(arg_format, value));
+ }
+
+ // Same as above but adds a runtime argument.
+ CmdlineBuilder& AddRuntime(const char* arg_format, int value)
+ __attribute__((enable_if(internal::ContainsOneFormatSpecifier(arg_format, 'd'),
+ "'arg' must be a string literal that contains '%d'"))) {
+ return AddRuntime(android::base::StringPrintf(arg_format, value));
+ }
+
+ // Adds a string value formatted by the format string if the value is non-empty. Does nothing
+ // otherwise.
+ //
+ // Usage: AddIfNonEmpty("--flag=%s", "value")
+ CmdlineBuilder& AddIfNonEmpty(const char* arg_format, const std::string& value)
+ __attribute__((enable_if(internal::ContainsOneFormatSpecifier(arg_format, 's'),
+ "'arg' must be a string literal that contains '%s'"))) {
+ if (!value.empty()) {
+ Add(android::base::StringPrintf(arg_format, value.c_str()));
+ }
+ return *this;
+ }
+
+ // Same as above but adds a runtime argument.
+ CmdlineBuilder& AddRuntimeIfNonEmpty(const char* arg_format, const std::string& value)
+ __attribute__((enable_if(internal::ContainsOneFormatSpecifier(arg_format, 's'),
+ "'arg' must be a string literal that contains '%s'"))) {
+ if (!value.empty()) {
+ AddRuntime(android::base::StringPrintf(arg_format, value.c_str()));
+ }
+ return *this;
+ }
+
+ // Adds an argument as-is if the boolean value is true. Does nothing otherwise.
+ CmdlineBuilder& AddIf(bool value, std::string_view arg) {
+ if (value) {
+ Add(arg);
+ }
+ return *this;
+ }
+
+ // Same as above but adds a runtime argument.
+ CmdlineBuilder& AddRuntimeIf(bool value, std::string_view arg) {
+ if (value) {
+ AddRuntime(arg);
+ }
+ return *this;
+ }
+
+ // Concatenates this builder with another. Returns the concatenated result and nullifies the input
+ // builder.
+ CmdlineBuilder& Concat(CmdlineBuilder&& other) {
+ elements_.reserve(elements_.size() + other.elements_.size());
+ std::move(other.elements_.begin(), other.elements_.end(), std::back_inserter(elements_));
+ other.elements_.clear();
+ return *this;
+ }
+
+ private:
+ std::vector<std::string> elements_;
+};
+
+} // namespace tools
+} // namespace art
+
+#endif // ART_LIBARTTOOLS_TOOLS_CMDLINE_BUILDER_H_
diff --git a/libarttools/tools/cmdline_builder_test.cc b/libarttools/tools/cmdline_builder_test.cc
new file mode 100644
index 0000000000..55518607db
--- /dev/null
+++ b/libarttools/tools/cmdline_builder_test.cc
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "cmdline_builder.h"
+
+#include <utility>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+
+namespace art {
+namespace tools {
+namespace {
+
+using ::testing::ElementsAre;
+using ::testing::IsEmpty;
+
+class CmdlineBuilderTest : public testing::Test {
+ protected:
+ CmdlineBuilder args_;
+};
+
+TEST_F(CmdlineBuilderTest, ContainsOneFormatSpecifier) {
+ EXPECT_TRUE(internal::ContainsOneFormatSpecifier("--flag=%s", 's'));
+ EXPECT_TRUE(internal::ContainsOneFormatSpecifier("--flag=[%s]", 's'));
+ EXPECT_TRUE(internal::ContainsOneFormatSpecifier("--flag=%s%%", 's'));
+ EXPECT_TRUE(internal::ContainsOneFormatSpecifier("--flag=[%s%%]", 's'));
+ EXPECT_TRUE(internal::ContainsOneFormatSpecifier("--flag=%%%s", 's'));
+ EXPECT_FALSE(internal::ContainsOneFormatSpecifier("--flag=", 's'));
+ EXPECT_FALSE(internal::ContainsOneFormatSpecifier("--flag=%s%s", 's'));
+ EXPECT_FALSE(internal::ContainsOneFormatSpecifier("--flag=%s%", 's'));
+ EXPECT_FALSE(internal::ContainsOneFormatSpecifier("--flag=%d", 's'));
+ EXPECT_FALSE(internal::ContainsOneFormatSpecifier("--flag=%s%d", 's'));
+ EXPECT_FALSE(internal::ContainsOneFormatSpecifier("--flag=%%s", 's'));
+}
+
+TEST_F(CmdlineBuilderTest, Add) {
+ args_.Add("--flag");
+ EXPECT_THAT(args_.Get(), ElementsAre("--flag"));
+}
+
+TEST_F(CmdlineBuilderTest, AddRuntime) {
+ args_.AddRuntime("--flag");
+ EXPECT_THAT(args_.Get(), ElementsAre("--runtime-arg", "--flag"));
+}
+
+TEST_F(CmdlineBuilderTest, AddString) {
+ args_.Add("--flag=[%s]", "foo");
+ EXPECT_THAT(args_.Get(), ElementsAre("--flag=[foo]"));
+}
+
+TEST_F(CmdlineBuilderTest, AddRuntimeString) {
+ args_.AddRuntime("--flag=[%s]", "foo");
+ EXPECT_THAT(args_.Get(), ElementsAre("--runtime-arg", "--flag=[foo]"));
+}
+
+TEST_F(CmdlineBuilderTest, AddInt) {
+ args_.Add("--flag=[%d]", 123);
+ EXPECT_THAT(args_.Get(), ElementsAre("--flag=[123]"));
+}
+
+TEST_F(CmdlineBuilderTest, AddRuntimeInt) {
+ args_.AddRuntime("--flag=[%d]", 123);
+ EXPECT_THAT(args_.Get(), ElementsAre("--runtime-arg", "--flag=[123]"));
+}
+
+TEST_F(CmdlineBuilderTest, AddIfNonEmpty) {
+ args_.AddIfNonEmpty("--flag=[%s]", "foo");
+ EXPECT_THAT(args_.Get(), ElementsAre("--flag=[foo]"));
+}
+
+TEST_F(CmdlineBuilderTest, AddIfNonEmptyEmpty) {
+ args_.AddIfNonEmpty("--flag=[%s]", "");
+ EXPECT_THAT(args_.Get(), IsEmpty());
+}
+
+TEST_F(CmdlineBuilderTest, AddRuntimeIfNonEmpty) {
+ args_.AddRuntimeIfNonEmpty("--flag=[%s]", "foo");
+ EXPECT_THAT(args_.Get(), ElementsAre("--runtime-arg", "--flag=[foo]"));
+}
+
+TEST_F(CmdlineBuilderTest, AddRuntimeIfNonEmptyEmpty) {
+ args_.AddRuntimeIfNonEmpty("--flag=[%s]", "");
+ EXPECT_THAT(args_.Get(), IsEmpty());
+}
+
+TEST_F(CmdlineBuilderTest, AddIfTrue) {
+ args_.AddIf(true, "--flag");
+ EXPECT_THAT(args_.Get(), ElementsAre("--flag"));
+}
+
+TEST_F(CmdlineBuilderTest, AddIfFalse) {
+ args_.AddIf(false, "--flag");
+ EXPECT_THAT(args_.Get(), IsEmpty());
+}
+
+TEST_F(CmdlineBuilderTest, AddRuntimeIfTrue) {
+ args_.AddRuntimeIf(true, "--flag");
+ EXPECT_THAT(args_.Get(), ElementsAre("--runtime-arg", "--flag"));
+}
+
+TEST_F(CmdlineBuilderTest, AddRuntimeIfFalse) {
+ args_.AddRuntimeIf(false, "--flag");
+ EXPECT_THAT(args_.Get(), IsEmpty());
+}
+
+TEST_F(CmdlineBuilderTest, Concat) {
+ args_.Add("--flag1");
+ args_.Add("--flag2");
+
+ CmdlineBuilder other;
+ other.Add("--flag3");
+ other.Add("--flag4");
+
+ args_.Concat(std::move(other));
+ EXPECT_THAT(args_.Get(), ElementsAre("--flag1", "--flag2", "--flag3", "--flag4"));
+ EXPECT_THAT(other.Get(), IsEmpty());
+}
+
+} // namespace
+} // namespace tools
+} // namespace art
diff --git a/libarttools/tools/system_properties.h b/libarttools/tools/system_properties.h
new file mode 100644
index 0000000000..06b7bcb340
--- /dev/null
+++ b/libarttools/tools/system_properties.h
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef ART_LIBARTTOOLS_TOOLS_SYSTEM_PROPERTIES_H_
+#define ART_LIBARTTOOLS_TOOLS_SYSTEM_PROPERTIES_H_
+
+#include <string>
+
+#include "android-base/parsebool.h"
+#include "android-base/properties.h"
+
+namespace art {
+namespace tools {
+
+// A class for getting system properties with fallback lookup support. Different from
+// android::base::GetProperty, this class is mockable.
+class SystemProperties {
+ public:
+ virtual ~SystemProperties() = default;
+
+ // Returns the current value of the system property `key`, or `default_value` if the property
+ // doesn't have a value.
+ std::string Get(const std::string& key, const std::string& default_value) const {
+ std::string value = GetProperty(key);
+ if (!value.empty()) {
+ return value;
+ }
+ return default_value;
+ }
+
+ // Same as above, but allows specifying one or more fallback keys. The last argument is a string
+ // default value that will be used if none of the given keys has a value.
+ //
+ // Usage:
+ //
+ // Look up for "key_1", then "key_2", then "key_3". If none of them has a value, return "default":
+ // Get("key_1", "key_2", "key_3", /*default_value=*/"default")
+ template <typename... Args>
+ std::string Get(const std::string& key, const std::string& fallback_key, Args... args) const {
+ return Get(key, Get(fallback_key, args...));
+ }
+
+ // Returns the current value of the system property `key` with zero or more fallback keys, or an
+ // empty string if none of the given keys has a value.
+ //
+ // Usage:
+ //
+ // Look up for "key_1". If it doesn't have a value, return an empty string:
+ // GetOrEmpty("key_1")
+ //
+ // Look up for "key_1", then "key_2", then "key_3". If none of them has a value, return an empty
+ // string:
+ // GetOrEmpty("key_1", "key_2", "key_3")
+ template <typename... Args>
+ std::string GetOrEmpty(const std::string& key, Args... fallback_keys) const {
+ return Get(key, fallback_keys..., /*default_value=*/"");
+ }
+
+ // Returns the current value of the boolean system property `key`, or `default_value` if the
+ // property doesn't have a value. See `android::base::ParseBool` for how the value is parsed.
+ bool GetBool(const std::string& key, bool default_value) const {
+ android::base::ParseBoolResult result = android::base::ParseBool(GetProperty(key));
+ if (result != android::base::ParseBoolResult::kError) {
+ return result == android::base::ParseBoolResult::kTrue;
+ }
+ return default_value;
+ }
+
+ // Same as above, but allows specifying one or more fallback keys. The last argument is a bool
+ // default value that will be used if none of the given keys has a value.
+ //
+ // Usage:
+ //
+ // Look up for "key_1", then "key_2", then "key_3". If none of them has a value, return true:
+ // Get("key_1", "key_2", "key_3", /*default_value=*/true)
+ template <typename... Args>
+ bool GetBool(const std::string& key, const std::string& fallback_key, Args... args) const {
+ return GetBool(key, GetBool(fallback_key, args...));
+ }
+
+ protected:
+ // The single source of truth of system properties. Can be mocked in unit tests.
+ virtual std::string GetProperty(const std::string& key) const {
+ return android::base::GetProperty(key, /*default_value=*/"");
+ }
+};
+
+} // namespace tools
+} // namespace art
+
+#endif // ART_LIBARTTOOLS_TOOLS_SYSTEM_PROPERTIES_H_
diff --git a/libarttools/tools/system_properties_test.cc b/libarttools/tools/system_properties_test.cc
new file mode 100644
index 0000000000..80300f0343
--- /dev/null
+++ b/libarttools/tools/system_properties_test.cc
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "system_properties.h"
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+
+namespace art {
+namespace tools {
+namespace {
+
+using ::testing::Return;
+
+class MockSystemProperties : public SystemProperties {
+ public:
+ MOCK_METHOD(std::string, GetProperty, (const std::string& key), (const, override));
+};
+
+class SystemPropertiesTest : public testing::Test {
+ protected:
+ MockSystemProperties system_properties_;
+};
+
+TEST_F(SystemPropertiesTest, Get) {
+ EXPECT_CALL(system_properties_, GetProperty("key_1")).WillOnce(Return("value_1"));
+ EXPECT_EQ(system_properties_.Get("key_1", /*default_value=*/"default"), "value_1");
+}
+
+TEST_F(SystemPropertiesTest, GetWithFallback) {
+ EXPECT_CALL(system_properties_, GetProperty("key_1")).WillOnce(Return(""));
+ EXPECT_CALL(system_properties_, GetProperty("key_2")).WillOnce(Return("value_2"));
+ EXPECT_CALL(system_properties_, GetProperty("key_3")).WillOnce(Return("value_3"));
+ EXPECT_EQ(system_properties_.Get("key_1", "key_2", "key_3", /*default_value=*/"default"),
+ "value_2");
+}
+
+TEST_F(SystemPropertiesTest, GetDefault) {
+ EXPECT_CALL(system_properties_, GetProperty("key_1")).WillOnce(Return(""));
+ EXPECT_EQ(system_properties_.Get("key_1", /*default_value=*/"default"), "default");
+}
+
+TEST_F(SystemPropertiesTest, GetOrEmpty) {
+ EXPECT_CALL(system_properties_, GetProperty("key_1")).WillOnce(Return("value_1"));
+ EXPECT_EQ(system_properties_.GetOrEmpty("key_1"), "value_1");
+}
+
+TEST_F(SystemPropertiesTest, GetOrEmptyWithFallback) {
+ EXPECT_CALL(system_properties_, GetProperty("key_1")).WillOnce(Return(""));
+ EXPECT_CALL(system_properties_, GetProperty("key_2")).WillOnce(Return("value_2"));
+ EXPECT_CALL(system_properties_, GetProperty("key_3")).WillOnce(Return("value_3"));
+ EXPECT_EQ(system_properties_.GetOrEmpty("key_1", "key_2", "key_3"), "value_2");
+}
+
+TEST_F(SystemPropertiesTest, GetOrEmptyDefault) {
+ EXPECT_CALL(system_properties_, GetProperty("key_1")).WillOnce(Return(""));
+ EXPECT_EQ(system_properties_.GetOrEmpty("key_1"), "");
+}
+
+TEST_F(SystemPropertiesTest, GetBoolTrue) {
+ EXPECT_CALL(system_properties_, GetProperty("key_1")).WillOnce(Return("true"));
+ EXPECT_EQ(system_properties_.GetBool("key_1", /*default_value=*/false), true);
+}
+
+TEST_F(SystemPropertiesTest, GetBoolFalse) {
+ EXPECT_CALL(system_properties_, GetProperty("key_1")).WillOnce(Return("false"));
+ EXPECT_EQ(system_properties_.GetBool("key_1", /*default_value=*/true), false);
+}
+
+TEST_F(SystemPropertiesTest, GetBoolWithFallback) {
+ EXPECT_CALL(system_properties_, GetProperty("key_1")).WillOnce(Return(""));
+ EXPECT_CALL(system_properties_, GetProperty("key_2")).WillOnce(Return("true"));
+ EXPECT_CALL(system_properties_, GetProperty("key_3")).WillOnce(Return("false"));
+ EXPECT_EQ(system_properties_.GetBool("key_1", "key_2", "key_3", /*default_value=*/false), true);
+}
+
+TEST_F(SystemPropertiesTest, GetBoolDefault) {
+ EXPECT_CALL(system_properties_, GetProperty("key_1")).WillOnce(Return(""));
+ EXPECT_EQ(system_properties_.GetBool("key_1", /*default_value=*/true), true);
+}
+
+} // namespace
+} // namespace tools
+} // namespace art
diff --git a/libarttools/tools/tools.cc b/libarttools/tools/tools.cc
index a3a91e81e2..3d5301ad7f 100644
--- a/libarttools/tools/tools.cc
+++ b/libarttools/tools/tools.cc
@@ -16,12 +16,129 @@
#include "tools.h"
+#include <errno.h>
+#include <fnmatch.h>
+
+#include <algorithm>
+#include <filesystem>
+#include <functional>
+#include <string>
+#include <string_view>
+#include <system_error>
+#include <vector>
+
+#include "android-base/logging.h"
+#include "fmt/format.h"
+
namespace art {
namespace tools {
-std::string getMsg() {
- return "hello world!";
+namespace {
+
+using ::std::placeholders::_1;
+
+using ::fmt::literals::operator""_format; // NOLINT
+
+// Returns true if `path_prefix` matches `pattern` or can be a prefix of a path that matches
+// `pattern` (i.e., `path_prefix` represents a directory that may contain a file whose path matches
+// `pattern`).
+bool PartialMatch(const std::filesystem::path& pattern, const std::filesystem::path& path_prefix) {
+ for (std::filesystem::path::const_iterator pattern_it = pattern.begin(),
+ path_prefix_it = path_prefix.begin();
+ ; // NOLINT
+ pattern_it++, path_prefix_it++) {
+ if (path_prefix_it == path_prefix.end()) {
+ return true;
+ }
+ if (pattern_it == pattern.end()) {
+ return false;
+ }
+ if (*pattern_it == "**") {
+ return true;
+ }
+ if (fnmatch(pattern_it->c_str(), path_prefix_it->c_str(), /*flags=*/0) != 0) {
+ return false;
+ }
+ }
}
+bool FullMatchRecursive(const std::filesystem::path& pattern,
+ std::filesystem::path::const_iterator pattern_it,
+ const std::filesystem::path& path,
+ std::filesystem::path::const_iterator path_it,
+ bool double_asterisk_visited = false) {
+ if (pattern_it == pattern.end() && path_it == path.end()) {
+ return true;
+ }
+ if (pattern_it == pattern.end()) {
+ return false;
+ }
+ if (*pattern_it == "**") {
+ DCHECK(!double_asterisk_visited);
+ std::filesystem::path::const_iterator next_pattern_it = pattern_it;
+ return FullMatchRecursive(
+ pattern, ++next_pattern_it, path, path_it, /*double_asterisk_visited=*/true) ||
+ (path_it != path.end() && FullMatchRecursive(pattern, pattern_it, path, ++path_it));
+ }
+ if (path_it == path.end()) {
+ return false;
+ }
+ if (fnmatch(pattern_it->c_str(), path_it->c_str(), /*flags=*/0) != 0) {
+ return false;
+ }
+ return FullMatchRecursive(pattern, ++pattern_it, path, ++path_it);
}
+
+// Returns true if `path` fully matches `pattern`.
+bool FullMatch(const std::filesystem::path& pattern, const std::filesystem::path& path) {
+ return FullMatchRecursive(pattern, pattern.begin(), path, path.begin());
}
+
+void MatchGlobRecursive(const std::vector<std::filesystem::path>& patterns,
+ const std::filesystem::path& root_dir,
+ /*out*/ std::vector<std::string>* results) {
+ std::error_code ec;
+ for (auto it = std::filesystem::recursive_directory_iterator(
+ root_dir, std::filesystem::directory_options::skip_permission_denied, ec);
+ !ec && it != std::filesystem::end(it);
+ it.increment(ec)) {
+ const std::filesystem::directory_entry& entry = *it;
+ if (std::none_of(patterns.begin(), patterns.end(), std::bind(PartialMatch, _1, entry.path()))) {
+ // Avoid unnecessary I/O and SELinux denials.
+ it.disable_recursion_pending();
+ continue;
+ }
+ std::error_code ec2;
+ if (entry.is_regular_file(ec2) &&
+ std::any_of(patterns.begin(), patterns.end(), std::bind(FullMatch, _1, entry.path()))) {
+ results->push_back(entry.path());
+ }
+ if (ec2) {
+ // It's expected that we don't have permission to stat some dirs/files, and we don't care
+ // about them.
+ if (ec2.value() != EACCES) {
+ LOG(ERROR) << "Unable to lstat '{}': {}"_format(entry.path().string(), ec2.message());
+ }
+ continue;
+ }
+ }
+ if (ec) {
+ LOG(ERROR) << "Unable to walk through '{}': {}"_format(root_dir.string(), ec.message());
+ }
+}
+
+} // namespace
+
+std::vector<std::string> Glob(const std::vector<std::string>& patterns, std::string_view root_dir) {
+ std::vector<std::filesystem::path> parsed_patterns;
+ parsed_patterns.reserve(patterns.size());
+ for (std::string_view pattern : patterns) {
+ parsed_patterns.emplace_back(pattern);
+ }
+ std::vector<std::string> results;
+ MatchGlobRecursive(parsed_patterns, root_dir, &results);
+ return results;
+}
+
+} // namespace tools
+} // namespace art
diff --git a/libarttools/tools/tools.h b/libarttools/tools/tools.h
index 8231f5f74a..c2bcee77a6 100644
--- a/libarttools/tools/tools.h
+++ b/libarttools/tools/tools.h
@@ -18,11 +18,24 @@
#define ART_LIBARTTOOLS_TOOLS_TOOLS_H_
#include <string>
+#include <string_view>
+#include <vector>
namespace art {
namespace tools {
-std::string getMsg();
+// Searches in a filesystem, starting from `root_dir`. Returns all regular files (i.e., excluding
+// directories, symlinks, etc.) that match at least one pattern in `patterns`. Each pattern is an
+// absolute path that contains zero or more wildcards. The scan does not follow symlinks to
+// directories.
+//
+// Supported wildcards are:
+// - Those documented in glob(7)
+// - '**': Matches zero or more path elements. This is only recognised by itself as a path segment.
+//
+// For simplicity and efficiency, at most one '**' is allowed.
+std::vector<std::string> Glob(const std::vector<std::string>& patterns,
+ std::string_view root_dir = "/");
} // namespace tools
} // namespace art
diff --git a/libarttools/tools/tools_test.cc b/libarttools/tools/tools_test.cc
index 6eaa8f60bb..2f61181c73 100644
--- a/libarttools/tools/tools_test.cc
+++ b/libarttools/tools/tools_test.cc
@@ -15,14 +15,101 @@
*/
#include "tools.h"
+
+#include <algorithm>
+#include <filesystem>
+#include <iterator>
+
+#include "android-base/file.h"
+#include "base/common_art_test.h"
+#include "gmock/gmock.h"
#include "gtest/gtest.h"
namespace art {
+namespace tools {
+namespace {
+
+using ::android::base::WriteStringToFile;
+using ::testing::UnorderedElementsAre;
+
+void CreateFile(const std::string& filename) {
+ std::filesystem::path path(filename);
+ std::filesystem::create_directories(path.parent_path());
+ ASSERT_TRUE(WriteStringToFile(/*content=*/"", filename));
+}
+
+class ArtToolsTest : public CommonArtTest {
+ protected:
+ void SetUp() override {
+ CommonArtTest::SetUp();
+ scratch_dir_ = std::make_unique<ScratchDir>();
+ scratch_path_ = scratch_dir_->GetPath();
+ // Remove the trailing '/';
+ scratch_path_.resize(scratch_path_.length() - 1);
+ }
+
+ void TearDown() override {
+ scratch_dir_.reset();
+ CommonArtTest::TearDown();
+ }
+
+ std::unique_ptr<ScratchDir> scratch_dir_;
+ std::string scratch_path_;
+};
+
+TEST_F(ArtToolsTest, Glob) {
+ CreateFile(scratch_path_ + "/abc/def/000.txt");
+ CreateFile(scratch_path_ + "/abc/def/ghi/123.txt");
+ CreateFile(scratch_path_ + "/abc/def/ghi/456.txt");
+ CreateFile(scratch_path_ + "/abc/def/ghi/456.pdf");
+ CreateFile(scratch_path_ + "/abc/def/ghi/jkl/456.txt");
+ CreateFile(scratch_path_ + "/789.txt");
+ CreateFile(scratch_path_ + "/abc/789.txt");
+ CreateFile(scratch_path_ + "/abc/aaa/789.txt");
+ CreateFile(scratch_path_ + "/abc/aaa/bbb/789.txt");
+ CreateFile(scratch_path_ + "/abc/mno/123.txt");
+ CreateFile(scratch_path_ + "/abc/aaa/mno/123.txt");
+ CreateFile(scratch_path_ + "/abc/aaa/bbb/mno/123.txt");
+ CreateFile(scratch_path_ + "/abc/aaa/bbb/mno/ccc/123.txt");
+ CreateFile(scratch_path_ + "/pqr/123.txt");
+ CreateFile(scratch_path_ + "/abc/pqr/123.txt");
+ CreateFile(scratch_path_ + "/abc/aaa/pqr/123.txt");
+ CreateFile(scratch_path_ + "/abc/aaa/bbb/pqr/123.txt");
+ CreateFile(scratch_path_ + "/abc/aaa/bbb/pqr/ccc/123.txt");
+ CreateFile(scratch_path_ + "/abc/aaa/bbb/pqr/ccc/ddd/123.txt");
+
+ // This symlink will cause infinite recursion. It should not be followed.
+ std::filesystem::create_directory_symlink(scratch_path_ + "/abc/aaa/bbb/pqr",
+ scratch_path_ + "/abc/aaa/bbb/pqr/lnk");
+
+ // This is a directory. It should not be included in the results.
+ std::filesystem::create_directory(scratch_path_ + "/abc/def/ghi/000.txt");
-class ArtToolsTest : public testing::Test {};
+ std::vector<std::string> patterns = {
+ scratch_path_ + "/abc/def/000.txt",
+ scratch_path_ + "/abc/def/ghi/*.txt",
+ scratch_path_ + "/abc/**/789.txt",
+ scratch_path_ + "/abc/**/mno/*.txt",
+ scratch_path_ + "/abc/**/pqr/**",
+ };
-TEST_F(ArtToolsTest, Hello) {
- EXPECT_EQ("hello world!", art::tools::getMsg());
+ EXPECT_THAT(Glob(patterns, scratch_path_),
+ UnorderedElementsAre(scratch_path_ + "/abc/def/000.txt",
+ scratch_path_ + "/abc/def/ghi/123.txt",
+ scratch_path_ + "/abc/def/ghi/456.txt",
+ scratch_path_ + "/abc/789.txt",
+ scratch_path_ + "/abc/aaa/789.txt",
+ scratch_path_ + "/abc/aaa/bbb/789.txt",
+ scratch_path_ + "/abc/mno/123.txt",
+ scratch_path_ + "/abc/aaa/mno/123.txt",
+ scratch_path_ + "/abc/aaa/bbb/mno/123.txt",
+ scratch_path_ + "/abc/pqr/123.txt",
+ scratch_path_ + "/abc/aaa/pqr/123.txt",
+ scratch_path_ + "/abc/aaa/bbb/pqr/123.txt",
+ scratch_path_ + "/abc/aaa/bbb/pqr/ccc/123.txt",
+ scratch_path_ + "/abc/aaa/bbb/pqr/ccc/ddd/123.txt"));
}
+} // namespace
+} // namespace tools
} // namespace art