diff --git a/.gitignore b/.gitignore index 0d4fed2..5a364af 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ bazel-* MODULE.bazel.lock +/.ijwb/ +/.clwb/ diff --git a/MODULE.bazel b/MODULE.bazel index 311dc50..978bd5c 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -13,5 +13,6 @@ use_repo(cc_configure, "local_config_cc", "local_config_cc_toolchains") register_toolchains("@local_config_cc_toolchains//:all") +bazel_dep(name = "googletest", version = "1.15.2", dev_dependency = True) bazel_dep(name = "rules_testing", version = "0.6.0", dev_dependency = True) bazel_dep(name = "stardoc", version = "0.7.0", dev_dependency = True) diff --git a/WORKSPACE b/WORKSPACE index 114cb06..02be83e 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -30,3 +30,10 @@ http_archive( strip_prefix = "rules_testing-0.6.0", url = "https://github.com/bazelbuild/rules_testing/releases/download/v0.6.0/rules_testing-v0.6.0.tar.gz", ) + +http_archive( + name = "googletest", + integrity = "sha256-e0K01u1IgQxTYsJloX+uvpDcI3PIheUhZDnTeSfwKSY=", + strip_prefix = "googletest-1.15.2", + url = "https://github.com/google/googletest/releases/download/v1.15.2/googletest-1.15.2.tar.gz", +) diff --git a/cc/runfiles/BUILD b/cc/runfiles/BUILD index 887e1f2..f960a4e 100644 --- a/cc/runfiles/BUILD +++ b/cc/runfiles/BUILD @@ -1,7 +1,14 @@ +load("@rules_cc//cc:cc_library.bzl", "cc_library") + licenses(["notice"]) -alias( +cc_library( name = "runfiles", - actual = "@bazel_tools//tools/cpp/runfiles", + srcs = ["runfiles.cc"], + hdrs = ["runfiles.h"], + # Ensure that this header can be included from the same directory as the + # legacy @bazel_tools//tools/cpp/runfiles library. + include_prefix = "tools/cpp/runfiles", + strip_include_prefix = ".", visibility = ["//visibility:public"], ) diff --git a/cc/runfiles/runfiles.cc b/cc/runfiles/runfiles.cc new file mode 100644 index 0000000..2dcc015 --- /dev/null +++ b/cc/runfiles/runfiles.cc @@ -0,0 +1,484 @@ +// Copyright 2018 The Bazel Authors. All rights reserved. +// +// 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 "tools/cpp/runfiles/runfiles.h" + +#ifdef _WIN32 +#include +#else // not _WIN32 +#include +#include +#include +#include +#endif // _WIN32 + +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#endif // _WIN32 + +namespace bazel { +namespace tools { +namespace cpp { +namespace runfiles { + +using std::function; +using std::map; +using std::pair; +using std::string; +using std::vector; + +namespace { + +bool starts_with(const string& s, const char* prefix) { + if (!prefix || !*prefix) { + return true; + } + if (s.empty()) { + return false; + } + return s.find(prefix) == 0; +} + +bool contains(const string& s, const char* substr) { + if (!substr || !*substr) { + return true; + } + if (s.empty()) { + return false; + } + return s.find(substr) != string::npos; +} + +bool ends_with(const string& s, const string& suffix) { + if (suffix.empty()) { + return true; + } + if (s.empty()) { + return false; + } + return s.rfind(suffix) == s.size() - suffix.size(); +} + +bool IsReadableFile(const string& path) { + return std::ifstream(path).is_open(); +} + +bool IsDirectory(const string& path) { +#ifdef _WIN32 + DWORD attrs = GetFileAttributesA(path.c_str()); + return (attrs != INVALID_FILE_ATTRIBUTES) && + (attrs & FILE_ATTRIBUTE_DIRECTORY); +#else + struct stat buf; + return stat(path.c_str(), &buf) == 0 && S_ISDIR(buf.st_mode); +#endif +} + +bool PathsFrom(const std::string& argv0, std::string runfiles_manifest_file, + std::string runfiles_dir, std::string* out_manifest, + std::string* out_directory); + +bool PathsFrom(const std::string& argv0, std::string runfiles_manifest_file, + std::string runfiles_dir, + std::function is_runfiles_manifest, + std::function is_runfiles_directory, + std::string* out_manifest, std::string* out_directory); + +bool ParseManifest(const string& path, map* result, + string* error); +bool ParseRepoMapping(const string& path, + map, string>* result, string* error); + +} // namespace + +Runfiles* Runfiles::Create(const string& argv0, + const string& runfiles_manifest_file, + const string& runfiles_dir, + const string& source_repository, string* error) { + string manifest, directory; + if (!PathsFrom(argv0, runfiles_manifest_file, runfiles_dir, &manifest, + &directory)) { + if (error) { + std::ostringstream err; + err << "ERROR: " << __FILE__ << "(" << __LINE__ + << "): cannot find runfiles (argv0=\"" << argv0 << "\")"; + *error = err.str(); + } + return nullptr; + } + + vector > envvars = { + {"RUNFILES_MANIFEST_FILE", manifest}, + {"RUNFILES_DIR", directory}, + // TODO(laszlocsomor): remove JAVA_RUNFILES once the Java launcher can + // pick up RUNFILES_DIR. + {"JAVA_RUNFILES", directory}}; + + map runfiles; + if (!manifest.empty()) { + if (!ParseManifest(manifest, &runfiles, error)) { + return nullptr; + } + } + + map, string> mapping; + if (!ParseRepoMapping( + RlocationUnchecked("_repo_mapping", runfiles, directory), &mapping, + error)) { + return nullptr; + } + + return new Runfiles(std::move(runfiles), std::move(directory), + std::move(mapping), std::move(envvars), + string(source_repository)); +} + +bool IsAbsolute(const string& path) { + if (path.empty()) { + return false; + } + char c = path.front(); + return (c == '/' && (path.size() < 2 || path[1] != '/')) || + (path.size() >= 3 && + ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) && + path[1] == ':' && (path[2] == '\\' || path[2] == '/')); +} + +string GetEnv(const string& key) { +#ifdef _WIN32 + DWORD size = ::GetEnvironmentVariableA(key.c_str(), nullptr, 0); + if (size == 0) { + return string(); // unset or empty envvar + } + std::unique_ptr value(new char[size]); + ::GetEnvironmentVariableA(key.c_str(), value.get(), size); + return value.get(); +#else + char* result = getenv(key.c_str()); + return (result == nullptr) ? string() : string(result); +#endif +} + +// Replaces \s, \n, and \b with their respective characters. +string Unescape(const string& path) { + string result; + result.reserve(path.size()); + for (size_t i = 0; i < path.size(); ++i) { + if (path[i] == '\\' && i + 1 < path.size()) { + switch (path[i + 1]) { + case 's': { + result.push_back(' '); + break; + } + case 'n': { + result.push_back('\n'); + break; + } + case 'b': { + result.push_back('\\'); + break; + } + default: { + result.push_back(path[i]); + result.push_back(path[i + 1]); + break; + } + } + ++i; + } else { + result.push_back(path[i]); + } + } + return result; +} + +string Runfiles::Rlocation(const string& path) const { + return Rlocation(path, source_repository_); +} + +string Runfiles::Rlocation(const string& path, + const string& source_repo) const { + if (path.empty() || starts_with(path, "../") || contains(path, "/..") || + starts_with(path, "./") || contains(path, "/./") || + ends_with(path, "/.") || contains(path, "//")) { + return string(); + } + if (IsAbsolute(path)) { + return path; + } + + string::size_type first_slash = path.find_first_of('/'); + if (first_slash == string::npos) { + return RlocationUnchecked(path, runfiles_map_, directory_); + } + string target_apparent = path.substr(0, first_slash); + auto target = + repo_mapping_.find(std::make_pair(source_repo, target_apparent)); + if (target == repo_mapping_.cend()) { + return RlocationUnchecked(path, runfiles_map_, directory_); + } + return RlocationUnchecked(target->second + path.substr(first_slash), + runfiles_map_, directory_); +} + +string Runfiles::RlocationUnchecked(const string& path, + const map& runfiles_map, + const string& directory) { + const auto exact_match = runfiles_map.find(path); + if (exact_match != runfiles_map.end()) { + return exact_match->second; + } + if (!runfiles_map.empty()) { + // If path references a runfile that lies under a directory that itself is a + // runfile, then only the directory is listed in the manifest. Look up all + // prefixes of path in the manifest and append the relative path from the + // prefix to the looked up path. + std::size_t prefix_end = path.size(); + while ((prefix_end = path.find_last_of('/', prefix_end - 1)) != + string::npos) { + const string prefix = path.substr(0, prefix_end); + const auto prefix_match = runfiles_map.find(prefix); + if (prefix_match != runfiles_map.end()) { + return prefix_match->second + "/" + path.substr(prefix_end + 1); + } + } + } + if (!directory.empty()) { + return directory + "/" + path; + } + return ""; +} + +namespace { + +bool ParseManifest(const string& path, map* result, + string* error) { + std::ifstream stm(path); + if (!stm.is_open()) { + if (error) { + std::ostringstream err; + err << "ERROR: " << __FILE__ << "(" << __LINE__ + << "): cannot open runfiles manifest \"" << path << "\""; + *error = err.str(); + } + return false; + } + string line; + std::getline(stm, line); + size_t line_count = 1; + while (!line.empty()) { + std::string source; + std::string target; + if (line[0] == ' ') { + // The link path contains escape sequences for spaces and backslashes. + string::size_type idx = line.find(' ', 1); + if (idx == string::npos) { + if (error) { + std::ostringstream err; + err << "ERROR: " << __FILE__ << "(" << __LINE__ + << "): bad runfiles manifest entry in \"" << path << "\" line #" + << line_count << ": \"" << line << "\""; + *error = err.str(); + } + return false; + } + source = Unescape(line.substr(1, idx - 1)); + target = Unescape(line.substr(idx + 1)); + } else { + string::size_type idx = line.find(' '); + if (idx == string::npos) { + if (error) { + std::ostringstream err; + err << "ERROR: " << __FILE__ << "(" << __LINE__ + << "): bad runfiles manifest entry in \"" << path << "\" line #" + << line_count << ": \"" << line << "\""; + *error = err.str(); + } + return false; + } + source = line.substr(0, idx); + target = line.substr(idx + 1); + } + (*result)[source] = target; + std::getline(stm, line); + ++line_count; + } + return true; +} + +bool ParseRepoMapping(const string& path, + map, string>* result, + string* error) { + std::ifstream stm(path); + if (!stm.is_open()) { + return true; + } + string line; + std::getline(stm, line); + size_t line_count = 1; + while (!line.empty()) { + string::size_type first_comma = line.find_first_of(','); + if (first_comma == string::npos) { + if (error) { + std::ostringstream err; + err << "ERROR: " << __FILE__ << "(" << __LINE__ + << "): bad repository mapping entry in \"" << path << "\" line #" + << line_count << ": \"" << line << "\""; + *error = err.str(); + } + return false; + } + string::size_type second_comma = line.find_first_of(',', first_comma + 1); + if (second_comma == string::npos) { + if (error) { + std::ostringstream err; + err << "ERROR: " << __FILE__ << "(" << __LINE__ + << "): bad repository mapping entry in \"" << path << "\" line #" + << line_count << ": \"" << line << "\""; + *error = err.str(); + } + return false; + } + + string source = line.substr(0, first_comma); + string target_apparent = + line.substr(first_comma + 1, second_comma - (first_comma + 1)); + string target = line.substr(second_comma + 1); + + (*result)[std::make_pair(source, target_apparent)] = target; + std::getline(stm, line); + ++line_count; + } + return true; +} + +} // namespace + +namespace testing { + +bool TestOnly_PathsFrom(const string& argv0, string mf, string dir, + function is_runfiles_manifest, + function is_runfiles_directory, + string* out_manifest, string* out_directory) { + return PathsFrom(argv0, mf, dir, is_runfiles_manifest, is_runfiles_directory, + out_manifest, out_directory); +} + +bool TestOnly_IsAbsolute(const string& path) { return IsAbsolute(path); } + +} // namespace testing + +Runfiles* Runfiles::Create(const std::string& argv0, + const std::string& runfiles_manifest_file, + const std::string& runfiles_dir, + std::string* error) { + return Runfiles::Create(argv0, runfiles_manifest_file, runfiles_dir, "", + error); +} + +Runfiles* Runfiles::Create(const string& argv0, const string& source_repository, + string* error) { + return Runfiles::Create(argv0, GetEnv("RUNFILES_MANIFEST_FILE"), + GetEnv("RUNFILES_DIR"), source_repository, error); +} + +Runfiles* Runfiles::Create(const string& argv0, string* error) { + return Runfiles::Create(argv0, "", error); +} + +Runfiles* Runfiles::CreateForTest(const string& source_repository, + std::string* error) { + return Runfiles::Create(std::string(), GetEnv("RUNFILES_MANIFEST_FILE"), + GetEnv("TEST_SRCDIR"), source_repository, error); +} + +Runfiles* Runfiles::CreateForTest(std::string* error) { + return Runfiles::CreateForTest("", error); +} + +namespace { + +bool PathsFrom(const string& argv0, string mf, string dir, string* out_manifest, + string* out_directory) { + return PathsFrom( + argv0, mf, dir, [](const string& path) { return IsReadableFile(path); }, + [](const string& path) { return IsDirectory(path); }, out_manifest, + out_directory); +} + +bool PathsFrom(const string& argv0, string mf, string dir, + function is_runfiles_manifest, + function is_runfiles_directory, + string* out_manifest, string* out_directory) { + out_manifest->clear(); + out_directory->clear(); + + bool mfValid = is_runfiles_manifest(mf); + bool dirValid = is_runfiles_directory(dir); + + if (!argv0.empty() && !mfValid && !dirValid) { + mf = argv0 + ".runfiles/MANIFEST"; + dir = argv0 + ".runfiles"; + mfValid = is_runfiles_manifest(mf); + dirValid = is_runfiles_directory(dir); + if (!mfValid) { + mf = argv0 + ".runfiles_manifest"; + mfValid = is_runfiles_manifest(mf); + } + } + + if (!mfValid && !dirValid) { + return false; + } + + if (!mfValid) { + mf = dir + "/MANIFEST"; + mfValid = is_runfiles_manifest(mf); + if (!mfValid) { + mf = dir + "_manifest"; + mfValid = is_runfiles_manifest(mf); + } + } + + if (!dirValid && + (ends_with(mf, ".runfiles_manifest") || ends_with(mf, "/MANIFEST"))) { + static const size_t kSubstrLen = 9; // "_manifest" or "/MANIFEST" + dir = mf.substr(0, mf.size() - kSubstrLen); + dirValid = is_runfiles_directory(dir); + } + + if (mfValid) { + *out_manifest = mf; + } + + if (dirValid) { + *out_directory = dir; + } + + return true; +} + +} // namespace + +} // namespace runfiles +} // namespace cpp +} // namespace tools +} // namespace bazel diff --git a/cc/runfiles/runfiles.h b/cc/runfiles/runfiles.h new file mode 100644 index 0000000..d428965 --- /dev/null +++ b/cc/runfiles/runfiles.h @@ -0,0 +1,266 @@ +// Copyright 2018 The Bazel Authors. All rights reserved. +// +// 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. + +// Runfiles lookup library for Bazel-built C++ binaries and tests. +// +// USAGE: +// 1. Depend on this runfiles library from your build rule: +// +// cc_binary( +// name = "my_binary", +// ... +// deps = ["@rules_cc//cc/runfiles"], +// ) +// +// 2. Include the runfiles library. +// +// #include "tools/cpp/runfiles/runfiles.h" +// +// using bazel::tools::cpp::runfiles::Runfiles; +// +// 3. Create a Runfiles object and use rlocation to look up runfile paths: +// +// int main(int argc, char** argv) { +// std::string error; +// std::unique_ptr runfiles( +// Runfiles::Create(argv[0], BAZEL_CURRENT_REPOSITORY, &error)); +// +// // Important: +// // If this is a test, use +// // Runfiles::CreateForTest(BAZEL_CURRENT_REPOSITORY, &error). +// +// if (runfiles == nullptr) { +// ... // error handling +// } +// std::string path = +// runfiles->Rlocation("my_workspace/path/to/my/data.txt"); +// ... +// +// The code above creates a Runfiles object and retrieves a runfile path. +// The BAZEL_CURRENT_REPOSITORY macro is available in every target that +// depends on the runfiles library. +// +// The Runfiles::Create function uses the runfiles manifest and the +// runfiles directory from the RUNFILES_MANIFEST_FILE and RUNFILES_DIR +// environment variables. If not present, the function looks for the +// manifest and directory near argv[0], the path of the main program. +// +// To start child processes that also need runfiles, you need to set the right +// environment variables for them: +// +// std::unique_ptr runfiles(Runfiles::Create( +// argv[0], BAZEL_CURRENT_REPOSITORY, &error)); +// +// std::string path = runfiles->Rlocation("path/to/binary")); +// if (!path.empty()) { +// ... // create "args" argument vector for execv +// const auto envvars = runfiles->EnvVars(); +// pid_t child = fork(); +// if (child) { +// int status; +// waitpid(child, &status, 0); +// } else { +// for (const auto i : envvars) { +// setenv(i.first.c_str(), i.second.c_str(), 1); +// } +// execv(args[0], args); +// } + +#ifndef TOOLS_CPP_RUNFILES_RUNFILES_H_ +#define TOOLS_CPP_RUNFILES_RUNFILES_H_ 1 + +#include +#include +#include +#include +#include + +namespace bazel { +namespace tools { +namespace cpp { +namespace runfiles { + +class Runfiles { + public: + virtual ~Runfiles() {} + + // Returns a new `Runfiles` instance. + // + // Use this from within `cc_test` rules. + // + // Returns nullptr on error. If `error` is provided, the method prints an + // error message into it. + // + // This method looks at the RUNFILES_MANIFEST_FILE and TEST_SRCDIR + // environment variables. + // + // If source_repository is not provided, it defaults to the main repository + // (also known as the workspace). + static Runfiles* CreateForTest(std::string* error = nullptr); + static Runfiles* CreateForTest(const std::string& source_repository, + std::string* error = nullptr); + + // Returns a new `Runfiles` instance. + // + // Use this from `cc_binary` or `cc_library` rules. You may pass an empty + // `argv0` if `argv[0]` from the `main` method is unknown. + // + // Returns nullptr on error. If `error` is provided, the method prints an + // error message into it. + // + // This method looks at the RUNFILES_MANIFEST_FILE and RUNFILES_DIR + // environment variables. If either is empty, the method looks for the + // manifest or directory using the other environment variable, or using argv0 + // (unless it's empty). + // + // If source_repository is not provided, it defaults to the main repository + // (also known as the workspace). + static Runfiles* Create(const std::string& argv0, + std::string* error = nullptr); + static Runfiles* Create(const std::string& argv0, + const std::string& source_repository, + std::string* error = nullptr); + + // Returns a new `Runfiles` instance. + // + // Use this from any `cc_*` rule if you want to manually specify the paths to + // the runfiles manifest and/or runfiles directory. You may pass an empty + // `argv0` if `argv[0]` from the `main` method is unknown. + // + // This method is the same as `Create(argv0, error)`, except it uses + // `runfiles_manifest_file` and `runfiles_dir` as the corresponding + // environment variable values, instead of looking up the actual environment + // variables. + static Runfiles* Create(const std::string& argv0, + const std::string& runfiles_manifest_file, + const std::string& runfiles_dir, + std::string* error = nullptr); + static Runfiles* Create(const std::string& argv0, + const std::string& runfiles_manifest_file, + const std::string& runfiles_dir, + const std::string& source_repository, + std::string* error = nullptr); + + // Returns the runtime path of a runfile. + // + // Runfiles are data-dependencies of Bazel-built binaries and tests. + // + // The returned path may not exist. The caller should verify the path's + // existence. + // + // The function may return an empty string if it cannot find a runfile. + // + // Args: + // path: runfiles-root-relative path of the runfile; must not be empty and + // must not contain uplevel references. + // source_repository: if provided, overrides the source repository set when + // this Runfiles instance was created. + // Returns: + // the path to the runfile, which the caller should check for existence, or + // an empty string if the method doesn't know about this runfile + std::string Rlocation(const std::string& path) const; + std::string Rlocation(const std::string& path, + const std::string& source_repository) const; + + // Returns environment variables for subprocesses. + // + // The caller should set the returned key-value pairs in the environment of + // subprocesses, so that those subprocesses can also access runfiles (in case + // they are also Bazel-built binaries). + const std::vector >& EnvVars() const { + return envvars_; + } + + // Returns a new Runfiles instance that by default uses the provided source + // repository as a default for all calls to Rlocation. + // + // The current instance remains valid. + std::unique_ptr WithSourceRepository( + const std::string& source_repository) const { + return std::unique_ptr(new Runfiles( + runfiles_map_, directory_, repo_mapping_, envvars_, source_repository)); + } + + private: + Runfiles( + std::map runfiles_map, std::string directory, + std::map, std::string> repo_mapping, + std::vector > envvars, + std::string source_repository) + : runfiles_map_(std::move(runfiles_map)), + directory_(std::move(directory)), + repo_mapping_(std::move(repo_mapping)), + envvars_(std::move(envvars)), + source_repository_(std::move(source_repository)) {} + Runfiles(const Runfiles&) = delete; + Runfiles(Runfiles&&) = delete; + Runfiles& operator=(const Runfiles&) = delete; + Runfiles& operator=(Runfiles&&) = delete; + + static std::string RlocationUnchecked( + const std::string& path, + const std::map& runfiles_map, + const std::string& directory); + + const std::map runfiles_map_; + const std::string directory_; + const std::map, std::string> + repo_mapping_; + const std::vector > envvars_; + const std::string source_repository_; +}; + +// The "testing" namespace contains functions that allow unit testing the code. +// Do not use these outside of runfiles_test.cc, they are only part of the +// public API for the benefit of the tests. +// These functions and their interface may change without notice. +namespace testing { + +// For testing only. +// +// Computes the path of the runfiles manifest and the runfiles directory. +// +// If the method finds both a valid manifest and valid directory according to +// `is_runfiles_manifest` and `is_runfiles_directory`, then the method sets +// the corresponding values to `out_manifest` and `out_directory` and returns +// true. +// +// If the method only finds a valid manifest or a valid directory, but not +// both, then it sets the corresponding output variable (`out_manifest` or +// `out_directory`) to the value while clearing the other output variable. The +// method still returns true in this case. +// +// If the method cannot find either a valid manifest or valid directory, it +// clears both output variables and returns false. +bool TestOnly_PathsFrom( + const std::string& argv0, std::string runfiles_manifest_file, + std::string runfiles_dir, + std::function is_runfiles_manifest, + std::function is_runfiles_directory, + std::string* out_manifest, std::string* out_directory); + +// For testing only. +// Returns true if `path` is an absolute Unix or Windows path. +// For Windows paths, this function does not regard drive-less absolute paths +// (i.e. absolute-on-current-drive, e.g. "\foo\bar") as absolute and returns +// false for these. +bool TestOnly_IsAbsolute(const std::string& path); + +} // namespace testing +} // namespace runfiles +} // namespace cpp +} // namespace tools +} // namespace bazel + +#endif // TOOLS_CPP_RUNFILES_RUNFILES_H_ diff --git a/tests/runfiles/BUILD b/tests/runfiles/BUILD new file mode 100644 index 0000000..46bb764 --- /dev/null +++ b/tests/runfiles/BUILD @@ -0,0 +1,10 @@ +load("@rules_cc//cc:cc_test.bzl", "cc_test") + +cc_test( + name = "runfiles_test", + srcs = ["runfiles_test.cc"], + deps = [ + "//cc/runfiles", + "@googletest//:gtest_main", + ], +) diff --git a/tests/runfiles/runfiles_test.cc b/tests/runfiles/runfiles_test.cc new file mode 100644 index 0000000..37391d6 --- /dev/null +++ b/tests/runfiles/runfiles_test.cc @@ -0,0 +1,882 @@ +// Copyright 2018 The Bazel Authors. All rights reserved. +// +// 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 "tools/cpp/runfiles/runfiles.h" + +#ifdef _WIN32 +#include +#endif // _WIN32 + +#include +#include +#include +#include + +#include "gtest/gtest.h" + +#define RUNFILES_TEST_TOSTRING_HELPER(x) #x +#define RUNFILES_TEST_TOSTRING(x) RUNFILES_TEST_TOSTRING_HELPER(x) +#define LINE_AS_STRING() RUNFILES_TEST_TOSTRING(__LINE__) + +namespace bazel { +namespace tools { +namespace cpp { +namespace runfiles { +namespace { + +using bazel::tools::cpp::runfiles::testing::TestOnly_IsAbsolute; +using bazel::tools::cpp::runfiles::testing::TestOnly_PathsFrom; +using std::cerr; +using std::endl; +using std::function; +using std::pair; +using std::string; +using std::unique_ptr; +using std::vector; + +class RunfilesTest : public ::testing::Test { + protected: + // Create a temporary file that is deleted with the destructor. + class MockFile { + public: + // Create an empty file with the given name under $TEST_TMPDIR. + static MockFile* Create(const string& name); + + // Create a file with the given name and contents under $TEST_TMPDIR. + // The method ensures to create all parent directories, so `name` is allowed + // to contain directory components. + static MockFile* Create(const string& name, const vector& lines); + + ~MockFile(); + const string& Path() const { return path_; } + + string DirName() const { + string::size_type pos = path_.find_last_of('/'); + return pos == string::npos ? "" : path_.substr(0, pos); + } + + private: + MockFile(const string& path) : path_(path) {} + MockFile(const MockFile&) = delete; + MockFile(MockFile&&) = delete; + MockFile& operator=(const MockFile&) = delete; + MockFile& operator=(MockFile&&) = delete; + + const string path_; + }; + + void AssertEnvvars(const Runfiles& runfiles, + const string& expected_manifest_file, + const string& expected_directory); + + static string GetTemp(); +}; + +void RunfilesTest::AssertEnvvars(const Runfiles& runfiles, + const string& expected_manifest_file, + const string& expected_directory) { + vector > expected = { + {"RUNFILES_MANIFEST_FILE", expected_manifest_file}, + {"RUNFILES_DIR", expected_directory}, + {"JAVA_RUNFILES", expected_directory}}; + ASSERT_EQ(runfiles.EnvVars(), expected); +} + +string RunfilesTest::GetTemp() { +#ifdef _WIN32 + DWORD size = ::GetEnvironmentVariableA("TEST_TMPDIR", nullptr, 0); + if (size == 0) { + return string(); // unset or empty envvar + } + unique_ptr value(new char[size]); + ::GetEnvironmentVariableA("TEST_TMPDIR", value.get(), size); + return value.get(); +#else + char* result = getenv("TEST_TMPDIR"); + return result != nullptr ? string(result) : string(); +#endif +} + +RunfilesTest::MockFile* RunfilesTest::MockFile::Create(const string& name) { + return Create(name, vector()); +} + +RunfilesTest::MockFile* RunfilesTest::MockFile::Create( + const string& name, const vector& lines) { + if (name.find("..") != string::npos || TestOnly_IsAbsolute(name)) { + cerr << "WARNING: " << __FILE__ << "(" << __LINE__ << "): bad name: \"" + << name << "\"" << endl; + return nullptr; + } + + string tmp(RunfilesTest::GetTemp()); + if (tmp.empty()) { + cerr << "WARNING: " << __FILE__ << "(" << __LINE__ + << "): $TEST_TMPDIR is empty" << endl; + return nullptr; + } + string path(tmp + "/" + name); + + string::size_type i = 0; +#ifdef _WIN32 + while ((i = name.find_first_of("/\\", i + 1)) != string::npos) { + string d = tmp + "\\" + name.substr(0, i); + if (!CreateDirectoryA(d.c_str(), nullptr)) { + cerr << "ERROR: " << __FILE__ << "(" << __LINE__ + << "): failed to create directory \"" << d << "\"" << endl; + return nullptr; + } + } +#else + while ((i = name.find_first_of('/', i + 1)) != string::npos) { + string d = tmp + "/" + name.substr(0, i); + if (mkdir(d.c_str(), 0777)) { + cerr << "ERROR: " << __FILE__ << "(" << __LINE__ + << "): failed to create directory \"" << d << "\"" << endl; + return nullptr; + } + } +#endif + + auto stm = std::ofstream(path); + for (auto i : lines) { + stm << i << std::endl; + } + return new MockFile(path); +} + +RunfilesTest::MockFile::~MockFile() { std::remove(path_.c_str()); } + +TEST_F(RunfilesTest, CreatesManifestBasedRunfilesFromManifestNextToBinary) { + unique_ptr mf(MockFile::Create( + "foo" LINE_AS_STRING() ".runfiles_manifest", {"a/b c/d"})); + ASSERT_TRUE(mf != nullptr); + string argv0(mf->Path().substr( + 0, mf->Path().size() - string(".runfiles_manifest").size())); + + string error; + unique_ptr r(Runfiles::Create(argv0, /*runfiles_manifest_file=*/"", + /*runfiles_dir=*/"", &error)); + ASSERT_TRUE(r != nullptr); + EXPECT_TRUE(error.empty()); + EXPECT_EQ(r->Rlocation("a/b"), "c/d"); + // We know it's manifest-based because it returns empty string for unknown + // paths. + EXPECT_EQ(r->Rlocation("unknown"), ""); + AssertEnvvars(*r, mf->Path(), ""); +} + +TEST_F(RunfilesTest, + CreatesManifestBasedRunfilesFromManifestInRunfilesDirectory) { + unique_ptr mf(MockFile::Create( + "foo" LINE_AS_STRING() ".runfiles/MANIFEST", {"a/b c/d"})); + ASSERT_TRUE(mf != nullptr); + string argv0(mf->Path().substr( + 0, mf->Path().size() - string(".runfiles/MANIFEST").size())); + + string error; + unique_ptr r(Runfiles::Create(argv0, /*runfiles_manifest_file=*/"", + /*runfiles_dir=*/"", &error)); + ASSERT_TRUE(r != nullptr); + EXPECT_TRUE(error.empty()); + EXPECT_EQ(r->Rlocation("a/b"), "c/d"); + EXPECT_EQ(r->Rlocation("foo"), argv0 + ".runfiles/foo"); + AssertEnvvars(*r, mf->Path(), argv0 + ".runfiles"); +} + +TEST_F(RunfilesTest, CreatesManifestBasedRunfilesFromEnvvar) { + unique_ptr mf(MockFile::Create( + "foo" LINE_AS_STRING() ".runfiles_manifest", {"a/b c/d"})); + ASSERT_TRUE(mf != nullptr); + + string error; + unique_ptr r(Runfiles::Create("ignore-argv0", mf->Path(), + "non-existent-runfiles_dir", &error)); + ASSERT_TRUE(r != nullptr); + EXPECT_TRUE(error.empty()); + EXPECT_EQ(r->Rlocation("a/b"), "c/d"); + // We know it's manifest-based because it returns empty string for unknown + // paths. + EXPECT_EQ(r->Rlocation("unknown"), ""); + AssertEnvvars(*r, mf->Path(), ""); +} + +TEST_F(RunfilesTest, CannotCreateManifestBasedRunfilesDueToBadManifest) { + unique_ptr mf(MockFile::Create( + "foo" LINE_AS_STRING() ".runfiles_manifest", {"a b", "nospace"})); + ASSERT_TRUE(mf != nullptr); + + string error; + unique_ptr r( + Runfiles::Create("ignore-argv0", mf->Path(), "", &error)); + EXPECT_EQ(r, nullptr); + EXPECT_NE(error.find("bad runfiles manifest entry"), string::npos); + EXPECT_NE(error.find("line #2: \"nospace\""), string::npos); +} + +TEST_F(RunfilesTest, ManifestBasedRunfilesRlocationAndEnvVars) { + unique_ptr mf( + MockFile::Create("foo" LINE_AS_STRING() ".runfiles_manifest", + { + "a/b c/d", + "e/f target path with spaces", + " h/\\si j k", + " dir\\swith\\sspaces l/m", + " h/\\n\\s\\bi j k \\n\\b", + "not_escaped with\\backslash and spaces", + })); + ASSERT_TRUE(mf != nullptr); + + string error; + unique_ptr r( + Runfiles::Create("ignore-argv0", mf->Path(), "", &error)); + + ASSERT_TRUE(r != nullptr); + EXPECT_TRUE(error.empty()); + EXPECT_EQ(r->Rlocation("a/b"), "c/d"); + EXPECT_EQ(r->Rlocation("c/d"), ""); + EXPECT_EQ(r->Rlocation(""), ""); + EXPECT_EQ(r->Rlocation("foo"), ""); + EXPECT_EQ(r->Rlocation("foo/"), ""); + EXPECT_EQ(r->Rlocation("foo/bar"), ""); + EXPECT_EQ(r->Rlocation("../foo"), ""); + EXPECT_EQ(r->Rlocation("foo/.."), ""); + EXPECT_EQ(r->Rlocation("foo/../bar"), ""); + EXPECT_EQ(r->Rlocation("./foo"), ""); + EXPECT_EQ(r->Rlocation("foo/."), ""); + EXPECT_EQ(r->Rlocation("foo/./bar"), ""); + EXPECT_EQ(r->Rlocation("//foo"), ""); + EXPECT_EQ(r->Rlocation("foo//"), ""); + EXPECT_EQ(r->Rlocation("foo//bar"), ""); + EXPECT_EQ(r->Rlocation("/Foo"), "/Foo"); + EXPECT_EQ(r->Rlocation("c:/Foo"), "c:/Foo"); + EXPECT_EQ(r->Rlocation("c:\\Foo"), "c:\\Foo"); + EXPECT_EQ(r->Rlocation("a/b/file"), "c/d/file"); + EXPECT_EQ(r->Rlocation("a/b/deeply/nested/file"), "c/d/deeply/nested/file"); + EXPECT_EQ(r->Rlocation("a/b/deeply/nested/file with spaces"), + "c/d/deeply/nested/file with spaces"); + EXPECT_EQ(r->Rlocation("e/f"), "target path with spaces"); + EXPECT_EQ(r->Rlocation("e/f/file"), "target path with spaces/file"); + EXPECT_EQ(r->Rlocation("h/ i"), "j k"); + EXPECT_EQ(r->Rlocation("h/\n \\i"), "j k \n\\"); + EXPECT_EQ(r->Rlocation("dir with spaces"), "l/m"); + EXPECT_EQ(r->Rlocation("dir with spaces/file"), "l/m/file"); + EXPECT_EQ(r->Rlocation("not_escaped"), "with\\backslash and spaces"); +} + +TEST_F(RunfilesTest, DirectoryBasedRunfilesRlocationAndEnvVars) { + unique_ptr dummy( + MockFile::Create("foo" LINE_AS_STRING() ".runfiles/dummy", {"a/b c/d"})); + ASSERT_TRUE(dummy != nullptr); + string dir = dummy->DirName(); + + string error; + unique_ptr r(Runfiles::Create("ignore-argv0", "", dir, &error)); + ASSERT_TRUE(r != nullptr); + EXPECT_TRUE(error.empty()); + + EXPECT_EQ(r->Rlocation("a/b"), dir + "/a/b"); + EXPECT_EQ(r->Rlocation("c/d"), dir + "/c/d"); + EXPECT_EQ(r->Rlocation(""), ""); + EXPECT_EQ(r->Rlocation("foo"), dir + "/foo"); + EXPECT_EQ(r->Rlocation("foo/"), dir + "/foo/"); + EXPECT_EQ(r->Rlocation("foo/bar"), dir + "/foo/bar"); + EXPECT_EQ(r->Rlocation("../foo"), ""); + EXPECT_EQ(r->Rlocation("foo/.."), ""); + EXPECT_EQ(r->Rlocation("foo/../bar"), ""); + EXPECT_EQ(r->Rlocation("./foo"), ""); + EXPECT_EQ(r->Rlocation("foo/."), ""); + EXPECT_EQ(r->Rlocation("foo/./bar"), ""); + EXPECT_EQ(r->Rlocation("//foo"), ""); + EXPECT_EQ(r->Rlocation("foo//"), ""); + EXPECT_EQ(r->Rlocation("foo//bar"), ""); + EXPECT_EQ(r->Rlocation("/Foo"), "/Foo"); + EXPECT_EQ(r->Rlocation("c:/Foo"), "c:/Foo"); + EXPECT_EQ(r->Rlocation("c:\\Foo"), "c:\\Foo"); + AssertEnvvars(*r, "", dir); +} + +TEST_F(RunfilesTest, ManifestAndDirectoryBasedRunfilesRlocationAndEnvVars) { + unique_ptr mf(MockFile::Create( + "foo" LINE_AS_STRING() ".runfiles/MANIFEST", {"a/b c/d"})); + ASSERT_TRUE(mf != nullptr); + string dir = mf->DirName(); + + string error; + unique_ptr r( + Runfiles::Create("ignore-argv0", mf->Path(), "", &error)); + + ASSERT_TRUE(r != nullptr); + EXPECT_TRUE(error.empty()); + EXPECT_EQ(r->Rlocation("a/b"), "c/d"); + EXPECT_EQ(r->Rlocation("c/d"), dir + "/c/d"); + EXPECT_EQ(r->Rlocation(""), ""); + EXPECT_EQ(r->Rlocation("foo"), dir + "/foo"); + EXPECT_EQ(r->Rlocation("foo/"), dir + "/foo/"); + EXPECT_EQ(r->Rlocation("foo/bar"), dir + "/foo/bar"); + EXPECT_EQ(r->Rlocation("../foo"), ""); + EXPECT_EQ(r->Rlocation("foo/.."), ""); + EXPECT_EQ(r->Rlocation("foo/../bar"), ""); + EXPECT_EQ(r->Rlocation("./foo"), ""); + EXPECT_EQ(r->Rlocation("foo/."), ""); + EXPECT_EQ(r->Rlocation("foo/./bar"), ""); + EXPECT_EQ(r->Rlocation("//foo"), ""); + EXPECT_EQ(r->Rlocation("foo//"), ""); + EXPECT_EQ(r->Rlocation("foo//bar"), ""); + EXPECT_EQ(r->Rlocation("/Foo"), "/Foo"); + EXPECT_EQ(r->Rlocation("c:/Foo"), "c:/Foo"); + EXPECT_EQ(r->Rlocation("c:\\Foo"), "c:\\Foo"); + EXPECT_EQ(r->Rlocation("a/b/file"), "c/d/file"); + EXPECT_EQ(r->Rlocation("a/b/deeply/nested/file"), "c/d/deeply/nested/file"); + AssertEnvvars(*r, mf->Path(), dir); +} + +TEST_F(RunfilesTest, ManifestBasedRunfilesEnvVars) { + unique_ptr mf( + MockFile::Create(string("foo" LINE_AS_STRING() ".runfiles_manifest"))); + ASSERT_TRUE(mf != nullptr); + + string error; + unique_ptr r( + Runfiles::Create("ignore-argv0", mf->Path(), "", &error)); + ASSERT_TRUE(r != nullptr); + EXPECT_TRUE(error.empty()); + + AssertEnvvars(*r, mf->Path(), ""); +} + +TEST_F(RunfilesTest, CreatesDirectoryBasedRunfilesFromDirectoryNextToBinary) { + // We create a directory as a side-effect of creating a mock file. + unique_ptr mf( + MockFile::Create(string("foo" LINE_AS_STRING() ".runfiles/dummy"))); + string argv0(mf->Path().substr( + 0, mf->Path().size() - string(".runfiles/dummy").size())); + + string error; + unique_ptr r(Runfiles::Create(argv0, /*runfiles_manifest_file=*/"", + /*runfiles_dir=*/"", &error)); + ASSERT_TRUE(r != nullptr); + EXPECT_TRUE(error.empty()); + + EXPECT_EQ(r->Rlocation("a/b"), argv0 + ".runfiles/a/b"); + // We know it's directory-based because it returns some result for unknown + // paths. + EXPECT_EQ(r->Rlocation("unknown"), argv0 + ".runfiles/unknown"); + AssertEnvvars(*r, "", argv0 + ".runfiles"); +} + +TEST_F(RunfilesTest, CreatesDirectoryBasedRunfilesFromEnvvar) { + // We create a directory as a side-effect of creating a mock file. + unique_ptr mf( + MockFile::Create(string("foo" LINE_AS_STRING() ".runfiles/dummy"))); + string dir = mf->DirName(); + + string error; + unique_ptr r(Runfiles::Create("ignore-argv0", "", dir, &error)); + ASSERT_TRUE(r != nullptr); + EXPECT_TRUE(error.empty()); + + EXPECT_EQ(r->Rlocation("a/b"), dir + "/a/b"); + EXPECT_EQ(r->Rlocation("foo"), dir + "/foo"); + EXPECT_EQ(r->Rlocation("/Foo"), "/Foo"); + EXPECT_EQ(r->Rlocation("c:/Foo"), "c:/Foo"); + EXPECT_EQ(r->Rlocation("c:\\Foo"), "c:\\Foo"); + AssertEnvvars(*r, "", dir); +} + +TEST_F(RunfilesTest, FailsToCreateAnyRunfilesBecauseEnvvarsAreNotDefined) { + unique_ptr mf( + MockFile::Create(string("foo" LINE_AS_STRING() ".runfiles/MANIFEST"))); + ASSERT_TRUE(mf != nullptr); + + string error; + unique_ptr r( + Runfiles::Create("ignore-argv0", mf->Path(), "whatever", &error)); + ASSERT_TRUE(r != nullptr); + EXPECT_TRUE(error.empty()); + + // We create a directory as a side-effect of creating a mock file. + mf.reset(MockFile::Create(string("foo" LINE_AS_STRING() ".runfiles/dummy"))); + r.reset(Runfiles::Create("ignore-argv0", "", mf->DirName(), &error)); + ASSERT_TRUE(r != nullptr); + EXPECT_TRUE(error.empty()); + + r.reset(Runfiles::Create("ignore-argv0", /*runfiles_manifest_file=*/"", + /*runfiles_dir=*/"", &error)); + EXPECT_EQ(r, nullptr); + EXPECT_NE(error.find("cannot find runfiles"), string::npos); +} + +TEST_F(RunfilesTest, MockFileTest) { + { + unique_ptr mf( + MockFile::Create(string("foo" LINE_AS_STRING() "/.."))); + EXPECT_TRUE(mf == nullptr); + } + + { + unique_ptr mf(MockFile::Create(string("/Foo" LINE_AS_STRING()))); + EXPECT_TRUE(mf == nullptr); + } + + { + unique_ptr mf( + MockFile::Create(string("C:/Foo" LINE_AS_STRING()))); + EXPECT_TRUE(mf == nullptr); + } + + string path; + { + unique_ptr mf( + MockFile::Create(string("foo" LINE_AS_STRING() "/bar1/qux"))); + ASSERT_TRUE(mf != nullptr); + path = mf->Path(); + + std::ifstream stm(path); + EXPECT_TRUE(stm.good()); + string actual; + stm >> actual; + EXPECT_TRUE(actual.empty()); + } + { + std::ifstream stm(path); + EXPECT_FALSE(stm.good()); + } + + { + unique_ptr mf(MockFile::Create( + string("foo" LINE_AS_STRING() "/bar2/qux"), vector())); + ASSERT_TRUE(mf != nullptr); + path = mf->Path(); + + std::ifstream stm(path); + EXPECT_TRUE(stm.good()); + string actual; + stm >> actual; + EXPECT_TRUE(actual.empty()); + } + { + std::ifstream stm(path); + EXPECT_FALSE(stm.good()); + } + + { + unique_ptr mf( + MockFile::Create(string("foo" LINE_AS_STRING() "/bar3/qux"), + {"hello world", "you are beautiful"})); + ASSERT_TRUE(mf != nullptr); + path = mf->Path(); + + std::ifstream stm(path); + EXPECT_TRUE(stm.good()); + string actual; + std::getline(stm, actual); + EXPECT_EQ("hello world", actual); + std::getline(stm, actual); + EXPECT_EQ("you are beautiful", actual); + std::getline(stm, actual); + EXPECT_EQ("", actual); + } + { + std::ifstream stm(path); + EXPECT_FALSE(stm.good()); + } +} + +TEST_F(RunfilesTest, IsAbsolute) { + EXPECT_FALSE(TestOnly_IsAbsolute("foo")); + EXPECT_FALSE(TestOnly_IsAbsolute("foo/bar")); + EXPECT_FALSE(TestOnly_IsAbsolute("\\foo")); + EXPECT_TRUE(TestOnly_IsAbsolute("c:\\foo")); + EXPECT_TRUE(TestOnly_IsAbsolute("c:/foo")); + EXPECT_TRUE(TestOnly_IsAbsolute("/foo")); + EXPECT_TRUE(TestOnly_IsAbsolute("x:\\foo")); + EXPECT_FALSE(TestOnly_IsAbsolute("::\\foo")); + EXPECT_FALSE(TestOnly_IsAbsolute("x\\foo")); + EXPECT_FALSE(TestOnly_IsAbsolute("x:")); + EXPECT_TRUE(TestOnly_IsAbsolute("x:\\")); +} + +TEST_F(RunfilesTest, PathsFromEnvVars) { + string mf, dir, rm; + + // Both envvars have a valid value. + EXPECT_TRUE(TestOnly_PathsFrom( + "argv0", "mock1.runfiles/MANIFEST", "mock2.runfiles", + [](const string& path) { return path == "mock1.runfiles/MANIFEST"; }, + [](const string& path) { return path == "mock2.runfiles"; }, &mf, &dir)); + EXPECT_EQ(mf, "mock1.runfiles/MANIFEST"); + EXPECT_EQ(dir, "mock2.runfiles"); + + // RUNFILES_MANIFEST_FILE is invalid but RUNFILES_DIR is good and there's a + // runfiles manifest in the runfiles directory. + EXPECT_TRUE(TestOnly_PathsFrom( + "argv0", "mock1.runfiles/MANIFEST", "mock2.runfiles", + [](const string& path) { return path == "mock2.runfiles/MANIFEST"; }, + [](const string& path) { return path == "mock2.runfiles"; }, &mf, &dir)); + EXPECT_EQ(mf, "mock2.runfiles/MANIFEST"); + EXPECT_EQ(dir, "mock2.runfiles"); + + // RUNFILES_MANIFEST_FILE is invalid but RUNFILES_DIR is good, but there's no + // runfiles manifest in the runfiles directory. + EXPECT_TRUE(TestOnly_PathsFrom( + "argv0", "mock1.runfiles/MANIFEST", "mock2.runfiles", + [](const string& path) { return false; }, + [](const string& path) { return path == "mock2.runfiles"; }, &mf, &dir)); + EXPECT_EQ(mf, ""); + EXPECT_EQ(dir, "mock2.runfiles"); + + // RUNFILES_DIR is invalid but RUNFILES_MANIFEST_FILE is good, and it is in + // a valid-looking runfiles directory. + EXPECT_TRUE(TestOnly_PathsFrom( + "argv0", "mock1.runfiles/MANIFEST", "mock2", + [](const string& path) { return path == "mock1.runfiles/MANIFEST"; }, + [](const string& path) { return path == "mock1.runfiles"; }, &mf, &dir)); + EXPECT_EQ(mf, "mock1.runfiles/MANIFEST"); + EXPECT_EQ(dir, "mock1.runfiles"); + + // RUNFILES_DIR is invalid but RUNFILES_MANIFEST_FILE is good, but it is not + // in any valid-looking runfiles directory. + EXPECT_TRUE(TestOnly_PathsFrom( + "argv0", "mock1/MANIFEST", "mock2", + [](const string& path) { return path == "mock1/MANIFEST"; }, + [](const string& path) { return false; }, &mf, &dir)); + EXPECT_EQ(mf, "mock1/MANIFEST"); + EXPECT_EQ(dir, ""); + + // Both envvars are invalid, but there's a manifest in a runfiles directory + // next to argv0, however there's no other content in the runfiles directory. + EXPECT_TRUE(TestOnly_PathsFrom( + "argv0", "mock1/MANIFEST", "mock2", + [](const string& path) { return path == "argv0.runfiles/MANIFEST"; }, + [](const string& path) { return false; }, &mf, &dir)); + EXPECT_EQ(mf, "argv0.runfiles/MANIFEST"); + EXPECT_EQ(dir, ""); + + // Both envvars are invalid, but there's a manifest next to argv0. There's + // no runfiles tree anywhere. + EXPECT_TRUE(TestOnly_PathsFrom( + "argv0", "mock1/MANIFEST", "mock2", + [](const string& path) { return path == "argv0.runfiles_manifest"; }, + [](const string& path) { return false; }, &mf, &dir)); + EXPECT_EQ(mf, "argv0.runfiles_manifest"); + EXPECT_EQ(dir, ""); + + // Both envvars are invalid, but there's a valid manifest next to argv0, and a + // valid runfiles directory (without a manifest in it). + EXPECT_TRUE(TestOnly_PathsFrom( + "argv0", "mock1/MANIFEST", "mock2", + [](const string& path) { return path == "argv0.runfiles_manifest"; }, + [](const string& path) { return path == "argv0.runfiles"; }, &mf, &dir)); + EXPECT_EQ(mf, "argv0.runfiles_manifest"); + EXPECT_EQ(dir, "argv0.runfiles"); + + // Both envvars are invalid, but there's a valid runfiles directory next to + // argv0, though no manifest in it. + EXPECT_TRUE(TestOnly_PathsFrom( + "argv0", "mock1/MANIFEST", "mock2", + [](const string& path) { return false; }, + [](const string& path) { return path == "argv0.runfiles"; }, &mf, &dir)); + EXPECT_EQ(mf, ""); + EXPECT_EQ(dir, "argv0.runfiles"); + + // Both envvars are invalid, but there's a valid runfiles directory next to + // argv0 with a valid manifest in it. + EXPECT_TRUE(TestOnly_PathsFrom( + "argv0", "mock1/MANIFEST", "mock2", + [](const string& path) { return path == "argv0.runfiles/MANIFEST"; }, + [](const string& path) { return path == "argv0.runfiles"; }, &mf, &dir)); + EXPECT_EQ(mf, "argv0.runfiles/MANIFEST"); + EXPECT_EQ(dir, "argv0.runfiles"); +} + +TEST_F(RunfilesTest, ManifestBasedRlocationWithRepoMapping_fromMain) { + string uid = LINE_AS_STRING(); + unique_ptr rm( + MockFile::Create("foo" + uid + ".repo_mapping", + {",config.json,config.json+1.2.3", ",my_module,_main", + ",my_protobuf,protobuf+3.19.2", ",my_workspace,_main", + "protobuf+3.19.2,config.json,config.json+1.2.3", + "protobuf+3.19.2,protobuf,protobuf+3.19.2"})); + ASSERT_TRUE(rm != nullptr); + unique_ptr mf(MockFile::Create( + "foo" + uid + ".runfiles_manifest", + {"_repo_mapping " + rm->Path(), "config.json /etc/config.json", + "protobuf+3.19.2/foo/runfile C:/Actual Path\\protobuf\\runfile", + "_main/bar/runfile /the/path/./to/other//other runfile.txt", + "protobuf+3.19.2/bar/dir E:\\Actual Path\\Directory"})); + ASSERT_TRUE(mf != nullptr); + string argv0(mf->Path().substr( + 0, mf->Path().size() - string(".runfiles_manifest").size())); + + string error; + unique_ptr r(Runfiles::Create(argv0, /*runfiles_manifest_file=*/"", + /*runfiles_dir=*/"", + /*source_repository=*/"", &error)); + ASSERT_TRUE(r != nullptr); + EXPECT_TRUE(error.empty()); + + EXPECT_EQ(r->Rlocation("my_module/bar/runfile"), + "/the/path/./to/other//other runfile.txt"); + EXPECT_EQ(r->Rlocation("my_workspace/bar/runfile"), + "/the/path/./to/other//other runfile.txt"); + EXPECT_EQ(r->Rlocation("my_protobuf/foo/runfile"), + "C:/Actual Path\\protobuf\\runfile"); + EXPECT_EQ(r->Rlocation("my_protobuf/bar/dir"), "E:\\Actual Path\\Directory"); + EXPECT_EQ(r->Rlocation("my_protobuf/bar/dir/file"), + "E:\\Actual Path\\Directory/file"); + EXPECT_EQ(r->Rlocation("my_protobuf/bar/dir/de eply/nes ted/fi+le"), + "E:\\Actual Path\\Directory/de eply/nes ted/fi+le"); + + EXPECT_EQ(r->Rlocation("protobuf/foo/runfile"), ""); + EXPECT_EQ(r->Rlocation("protobuf/bar/dir"), ""); + EXPECT_EQ(r->Rlocation("protobuf/bar/dir/file"), ""); + EXPECT_EQ(r->Rlocation("protobuf/bar/dir/dir/de eply/nes ted/fi+le"), ""); + + EXPECT_EQ(r->Rlocation("_main/bar/runfile"), + "/the/path/./to/other//other runfile.txt"); + EXPECT_EQ(r->Rlocation("protobuf+3.19.2/foo/runfile"), + "C:/Actual Path\\protobuf\\runfile"); + EXPECT_EQ(r->Rlocation("protobuf+3.19.2/bar/dir"), + "E:\\Actual Path\\Directory"); + EXPECT_EQ(r->Rlocation("protobuf+3.19.2/bar/dir/file"), + "E:\\Actual Path\\Directory/file"); + EXPECT_EQ(r->Rlocation("protobuf+3.19.2/bar/dir/de eply/nes ted/fi+le"), + "E:\\Actual Path\\Directory/de eply/nes ted/fi+le"); + + EXPECT_EQ(r->Rlocation("config.json"), "/etc/config.json"); + EXPECT_EQ(r->Rlocation("_main"), ""); + EXPECT_EQ(r->Rlocation("my_module"), ""); + EXPECT_EQ(r->Rlocation("protobuf"), ""); +} + +TEST_F(RunfilesTest, ManifestBasedRlocationWithRepoMapping_fromOtherRepo) { + string uid = LINE_AS_STRING(); + unique_ptr rm( + MockFile::Create("foo" + uid + ".repo_mapping", + {",config.json,config.json+1.2.3", ",my_module,_main", + ",my_protobuf,protobuf+3.19.2", ",my_workspace,_main", + "protobuf+3.19.2,config.json,config.json+1.2.3", + "protobuf+3.19.2,protobuf,protobuf+3.19.2"})); + ASSERT_TRUE(rm != nullptr); + unique_ptr mf(MockFile::Create( + "foo" + uid + ".runfiles_manifest", + {"_repo_mapping " + rm->Path(), "config.json /etc/config.json", + "protobuf+3.19.2/foo/runfile C:/Actual Path\\protobuf\\runfile", + "_main/bar/runfile /the/path/./to/other//other runfile.txt", + "protobuf+3.19.2/bar/dir E:\\Actual Path\\Directory"})); + ASSERT_TRUE(mf != nullptr); + string argv0(mf->Path().substr( + 0, mf->Path().size() - string(".runfiles_manifest").size())); + + string error; + unique_ptr r(Runfiles::Create(argv0, /*runfiles_manifest_file=*/"", + /*runfiles_dir=*/"", + "protobuf+3.19.2", &error)); + ASSERT_TRUE(r != nullptr); + EXPECT_TRUE(error.empty()); + + EXPECT_EQ(r->Rlocation("protobuf/foo/runfile"), + "C:/Actual Path\\protobuf\\runfile"); + EXPECT_EQ(r->Rlocation("protobuf/bar/dir"), "E:\\Actual Path\\Directory"); + EXPECT_EQ(r->Rlocation("protobuf/bar/dir/file"), + "E:\\Actual Path\\Directory/file"); + EXPECT_EQ(r->Rlocation("protobuf/bar/dir/de eply/nes ted/fi+le"), + "E:\\Actual Path\\Directory/de eply/nes ted/fi+le"); + + EXPECT_EQ(r->Rlocation("my_module/bar/runfile"), ""); + EXPECT_EQ(r->Rlocation("my_protobuf/foo/runfile"), ""); + EXPECT_EQ(r->Rlocation("my_protobuf/bar/dir"), ""); + EXPECT_EQ(r->Rlocation("my_protobuf/bar/dir/file"), ""); + EXPECT_EQ(r->Rlocation("my_protobuf/bar/dir/de eply/nes ted/fi+le"), ""); + + EXPECT_EQ(r->Rlocation("_main/bar/runfile"), + "/the/path/./to/other//other runfile.txt"); + EXPECT_EQ(r->Rlocation("protobuf+3.19.2/foo/runfile"), + "C:/Actual Path\\protobuf\\runfile"); + EXPECT_EQ(r->Rlocation("protobuf+3.19.2/bar/dir"), + "E:\\Actual Path\\Directory"); + EXPECT_EQ(r->Rlocation("protobuf+3.19.2/bar/dir/file"), + "E:\\Actual Path\\Directory/file"); + EXPECT_EQ(r->Rlocation("protobuf+3.19.2/bar/dir/de eply/nes ted/fi+le"), + "E:\\Actual Path\\Directory/de eply/nes ted/fi+le"); + + EXPECT_EQ(r->Rlocation("config.json"), "/etc/config.json"); + EXPECT_EQ(r->Rlocation("_main"), ""); + EXPECT_EQ(r->Rlocation("my_module"), ""); + EXPECT_EQ(r->Rlocation("protobuf"), ""); +} + +TEST_F(RunfilesTest, DirectoryBasedRlocationWithRepoMapping_fromMain) { + string uid = LINE_AS_STRING(); + unique_ptr rm( + MockFile::Create("foo" + uid + ".runfiles/_repo_mapping", + {",config.json,config.json+1.2.3", ",my_module,_main", + ",my_protobuf,protobuf+3.19.2", ",my_workspace,_main", + "protobuf+3.19.2,config.json,config.json+1.2.3", + "protobuf+3.19.2,protobuf,protobuf+3.19.2"})); + ASSERT_TRUE(rm != nullptr); + string dir = rm->DirName(); + string argv0(dir.substr(0, dir.size() - string(".runfiles").size())); + + string error; + unique_ptr r(Runfiles::Create(argv0, /*runfiles_manifest_file=*/"", + /*runfiles_dir=*/"", + /*source_repository=*/"", &error)); + ASSERT_TRUE(r != nullptr); + EXPECT_TRUE(error.empty()); + + EXPECT_EQ(r->Rlocation("my_module/bar/runfile"), dir + "/_main/bar/runfile"); + EXPECT_EQ(r->Rlocation("my_workspace/bar/runfile"), + dir + "/_main/bar/runfile"); + EXPECT_EQ(r->Rlocation("my_protobuf/foo/runfile"), + dir + "/protobuf+3.19.2/foo/runfile"); + EXPECT_EQ(r->Rlocation("my_protobuf/bar/dir"), + dir + "/protobuf+3.19.2/bar/dir"); + EXPECT_EQ(r->Rlocation("my_protobuf/bar/dir/file"), + dir + "/protobuf+3.19.2/bar/dir/file"); + EXPECT_EQ(r->Rlocation("my_protobuf/bar/dir/de eply/nes ted/fi+le"), + dir + "/protobuf+3.19.2/bar/dir/de eply/nes ted/fi+le"); + + EXPECT_EQ(r->Rlocation("protobuf/foo/runfile"), + dir + "/protobuf/foo/runfile"); + EXPECT_EQ(r->Rlocation("protobuf/bar/dir/dir/de eply/nes ted/fi+le"), + dir + "/protobuf/bar/dir/dir/de eply/nes ted/fi+le"); + + EXPECT_EQ(r->Rlocation("_main/bar/runfile"), dir + "/_main/bar/runfile"); + EXPECT_EQ(r->Rlocation("protobuf+3.19.2/foo/runfile"), + dir + "/protobuf+3.19.2/foo/runfile"); + EXPECT_EQ(r->Rlocation("protobuf+3.19.2/bar/dir"), + dir + "/protobuf+3.19.2/bar/dir"); + EXPECT_EQ(r->Rlocation("protobuf+3.19.2/bar/dir/file"), + dir + "/protobuf+3.19.2/bar/dir/file"); + EXPECT_EQ(r->Rlocation("protobuf+3.19.2/bar/dir/de eply/nes ted/fi+le"), + dir + "/protobuf+3.19.2/bar/dir/de eply/nes ted/fi+le"); + + EXPECT_EQ(r->Rlocation("config.json"), dir + "/config.json"); +} + +TEST_F(RunfilesTest, DirectoryBasedRlocationWithRepoMapping_fromOtherRepo) { + string uid = LINE_AS_STRING(); + unique_ptr rm( + MockFile::Create("foo" + uid + ".runfiles/_repo_mapping", + {",config.json,config.json+1.2.3", ",my_module,_main", + ",my_protobuf,protobuf+3.19.2", ",my_workspace,_main", + "protobuf+3.19.2,config.json,config.json+1.2.3", + "protobuf+3.19.2,protobuf,protobuf+3.19.2"})); + ASSERT_TRUE(rm != nullptr); + string dir = rm->DirName(); + string argv0(dir.substr(0, dir.size() - string(".runfiles").size())); + + string error; + unique_ptr r(Runfiles::Create(argv0, /*runfiles_manifest_file=*/"", + /*runfiles_dir=*/"", + "protobuf+3.19.2", &error)); + ASSERT_TRUE(r != nullptr); + EXPECT_TRUE(error.empty()); + + EXPECT_EQ(r->Rlocation("protobuf/foo/runfile"), + dir + "/protobuf+3.19.2/foo/runfile"); + EXPECT_EQ(r->Rlocation("protobuf/bar/dir"), dir + "/protobuf+3.19.2/bar/dir"); + EXPECT_EQ(r->Rlocation("protobuf/bar/dir/file"), + dir + "/protobuf+3.19.2/bar/dir/file"); + EXPECT_EQ(r->Rlocation("protobuf/bar/dir/de eply/nes ted/fi+le"), + dir + "/protobuf+3.19.2/bar/dir/de eply/nes ted/fi+le"); + + EXPECT_EQ(r->Rlocation("my_module/bar/runfile"), + dir + "/my_module/bar/runfile"); + EXPECT_EQ(r->Rlocation("my_protobuf/bar/dir/de eply/nes ted/fi+le"), + dir + "/my_protobuf/bar/dir/de eply/nes ted/fi+le"); + + EXPECT_EQ(r->Rlocation("_main/bar/runfile"), dir + "/_main/bar/runfile"); + EXPECT_EQ(r->Rlocation("protobuf+3.19.2/foo/runfile"), + dir + "/protobuf+3.19.2/foo/runfile"); + EXPECT_EQ(r->Rlocation("protobuf+3.19.2/bar/dir"), + dir + "/protobuf+3.19.2/bar/dir"); + EXPECT_EQ(r->Rlocation("protobuf+3.19.2/bar/dir/file"), + dir + "/protobuf+3.19.2/bar/dir/file"); + EXPECT_EQ(r->Rlocation("protobuf+3.19.2/bar/dir/de eply/nes ted/fi+le"), + dir + "/protobuf+3.19.2/bar/dir/de eply/nes ted/fi+le"); + + EXPECT_EQ(r->Rlocation("config.json"), dir + "/config.json"); +} + +TEST_F(RunfilesTest, + DirectoryBasedRlocationWithRepoMapping_fromOtherRepo_withSourceRepo) { + string uid = LINE_AS_STRING(); + unique_ptr rm( + MockFile::Create("foo" + uid + ".runfiles/_repo_mapping", + {",config.json,config.json+1.2.3", ",my_module,_main", + ",my_protobuf,protobuf+3.19.2", ",my_workspace,_main", + "protobuf+3.19.2,config.json,config.json+1.2.3", + "protobuf+3.19.2,protobuf,protobuf+3.19.2"})); + ASSERT_TRUE(rm != nullptr); + string dir = rm->DirName(); + string argv0(dir.substr(0, dir.size() - string(".runfiles").size())); + + string error; + unique_ptr r(Runfiles::Create(argv0, /*runfiles_manifest_file=*/"", + /*runfiles_dir=*/"", + /*source_repository=*/"", &error)); + r = r->WithSourceRepository("protobuf+3.19.2"); + ASSERT_TRUE(r != nullptr); + EXPECT_TRUE(error.empty()); + + EXPECT_EQ(r->Rlocation("protobuf/foo/runfile"), + dir + "/protobuf+3.19.2/foo/runfile"); + EXPECT_EQ(r->Rlocation("protobuf/bar/dir"), dir + "/protobuf+3.19.2/bar/dir"); + EXPECT_EQ(r->Rlocation("protobuf/bar/dir/file"), + dir + "/protobuf+3.19.2/bar/dir/file"); + EXPECT_EQ(r->Rlocation("protobuf/bar/dir/de eply/nes ted/fi+le"), + dir + "/protobuf+3.19.2/bar/dir/de eply/nes ted/fi+le"); + + EXPECT_EQ(r->Rlocation("my_module/bar/runfile"), + dir + "/my_module/bar/runfile"); + EXPECT_EQ(r->Rlocation("my_protobuf/bar/dir/de eply/nes ted/fi+le"), + dir + "/my_protobuf/bar/dir/de eply/nes ted/fi+le"); + + EXPECT_EQ(r->Rlocation("_main/bar/runfile"), dir + "/_main/bar/runfile"); + EXPECT_EQ(r->Rlocation("protobuf+3.19.2/foo/runfile"), + dir + "/protobuf+3.19.2/foo/runfile"); + EXPECT_EQ(r->Rlocation("protobuf+3.19.2/bar/dir"), + dir + "/protobuf+3.19.2/bar/dir"); + EXPECT_EQ(r->Rlocation("protobuf+3.19.2/bar/dir/file"), + dir + "/protobuf+3.19.2/bar/dir/file"); + EXPECT_EQ(r->Rlocation("protobuf+3.19.2/bar/dir/de eply/nes ted/fi+le"), + dir + "/protobuf+3.19.2/bar/dir/de eply/nes ted/fi+le"); + + EXPECT_EQ(r->Rlocation("config.json"), dir + "/config.json"); +} + +TEST_F(RunfilesTest, InvalidRepoMapping) { + string uid = LINE_AS_STRING(); + unique_ptr rm( + MockFile::Create("foo" + uid + ".runfiles/_repo_mapping", {"a,b"})); + ASSERT_TRUE(rm != nullptr); + string dir = rm->DirName(); + string argv0(dir.substr(0, dir.size() - string(".runfiles").size())); + + string error; + unique_ptr r(Runfiles::Create(argv0, /*runfiles_manifest_file=*/"", + /*runfiles_dir=*/"", + /*source_repository=*/"", &error)); + EXPECT_EQ(r, nullptr); + EXPECT_TRUE(error.find("bad repository mapping") != string::npos); +} + +} // namespace +} // namespace runfiles +} // namespace cpp +} // namespace tools +} // namespace bazel