kythe.io@v0.0.68-0.20240422202219-7225dbc01741/kythe/cxx/extractor/testlib.cc (about) 1 /* 2 * Copyright 2019 The Kythe Authors. All rights reserved. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 #include "kythe/cxx/extractor/testlib.h" 18 19 #include <errno.h> 20 #include <unistd.h> 21 22 #include <cstddef> 23 #include <cstring> 24 #include <memory> 25 #include <optional> 26 #include <string> 27 #include <utility> 28 #include <vector> 29 30 #include "absl/container/flat_hash_map.h" 31 #include "absl/log/check.h" 32 #include "absl/log/log.h" 33 #include "absl/strings/str_cat.h" 34 #include "absl/strings/string_view.h" 35 #include "absl/utility/utility.h" 36 #include "google/protobuf/any.pb.h" 37 #include "google/protobuf/message.h" 38 #include "google/protobuf/text_format.h" 39 #include "gtest/gtest.h" 40 #include "kythe/cxx/common/kzip_reader.h" 41 #include "kythe/cxx/common/path_utils.h" 42 #include "kythe/proto/analysis.pb.h" 43 #include "kythe/proto/cxx.pb.h" 44 #include "kythe/proto/filecontext.pb.h" 45 #include "llvm/ADT/StringRef.h" 46 #include "llvm/Support/Program.h" 47 #include "tools/cpp/runfiles/runfiles.h" 48 49 namespace kythe { 50 namespace { 51 namespace gpb = ::google::protobuf; 52 namespace kpb = ::kythe::proto; 53 using HashMap = ::absl::flat_hash_map<std::string, std::size_t>; 54 using ::bazel::tools::cpp::runfiles::Runfiles; 55 56 // Path prefix joined to runfiles to form the workspace-relative path. 57 constexpr absl::string_view kWorkspaceRoot = "io_kythe"; 58 59 constexpr absl::string_view kExtractorPath = 60 "kythe/cxx/extractor/cxx_extractor"; 61 62 void CanonicalizeHash(HashMap* hashes, std::string* hash) { 63 auto inserted = hashes->insert({*hash, hashes->size()}); 64 *hash = absl::StrCat("hash", inserted.first->second); 65 } 66 67 /// \brief Range wrapper around ContextDependentVersion, if any. 68 class MutableContextRows { 69 public: 70 using iterator = decltype(std::declval<kpb::ContextDependentVersion>() 71 .mutable_row() 72 ->begin()); 73 explicit MutableContextRows(kpb::CompilationUnit::FileInput* file_input) { 74 for (gpb::Any& detail : *file_input->mutable_details()) { 75 if (detail.UnpackTo(&context_)) { 76 any_ = &detail; 77 } 78 } 79 } 80 81 ~MutableContextRows() { 82 if (any_ != nullptr) { 83 any_->PackFrom(context_); 84 } 85 } 86 87 iterator begin() { return context_.mutable_row()->begin(); } 88 iterator end() { return context_.mutable_row()->end(); } 89 90 private: 91 gpb::Any* any_ = nullptr; 92 kpb::ContextDependentVersion context_; 93 }; 94 95 // RAII class for changing working directory. 96 // Ideally, this would be done after fork(), but before exec() except this isn't 97 // supported by LLVM ExecuteAndWait. 98 class ScopedWorkingDirectory { 99 public: 100 explicit ScopedWorkingDirectory(std::string working_directory) 101 : saved_working_directory_( 102 working_directory.empty() ? "" : GetCurrentDirectory().value()) { 103 if (working_directory.empty()) return; 104 CHECK(::chdir(working_directory.c_str()) == 0) 105 << "unable to chdir(" << working_directory << "): " << strerror(errno); 106 } 107 ~ScopedWorkingDirectory() { 108 if (saved_working_directory_.empty()) return; 109 110 CHECK(::chdir(saved_working_directory_.c_str()) == 0) 111 << "unable to chdir(" << saved_working_directory_ 112 << "): " << strerror(errno); 113 } 114 115 private: 116 std::string saved_working_directory_; 117 }; 118 119 // RAII class which removes the designated file name on destruction. 120 class TemporaryKzipFile { 121 public: 122 explicit TemporaryKzipFile() : filename_(KzipFileName()) {} 123 124 const std::string& filename() const { return filename_; }; 125 ~TemporaryKzipFile() { 126 if (::unlink(filename_.c_str()) != 0) { 127 LOG(ERROR) << "unable to remove " << filename_ << ": " << strerror(errno); 128 } 129 std::string::size_type slash = filename_.find_last_of('/'); 130 if (slash != std::string::npos) { 131 std::string dirname = filename_.substr(0, slash); 132 if (::rmdir(dirname.c_str()) != 0) { 133 LOG(ERROR) << "unable to remove " << dirname << ": " << strerror(errno); 134 } 135 } 136 } 137 138 private: 139 static std::string KzipFileName() { 140 const testing::TestInfo* test_info = 141 testing::UnitTest::GetInstance()->current_test_info(); 142 std::string testname = 143 test_info 144 ? absl::StrCat(test_info->test_suite_name(), "_", test_info->name()) 145 : "kzip_test"; 146 147 std::string base = 148 absl::StrCat(JoinPath(testing::TempDir(), testname), "XXXXXX"); 149 150 CHECK(mkdtemp(&base.front()) != nullptr) 151 << "unable to create temporary directory: " << strerror(errno); 152 153 return JoinPath(base, "test.kzip"); 154 } 155 156 std::string filename_; 157 }; 158 159 // Transforms std::vector<std::string> into a null-terminated array of c-string 160 // pointers suitable for passing to posix_spawn. 161 std::vector<llvm::StringRef> VectorForExecute( 162 const std::vector<std::string>& values) { 163 std::vector<llvm::StringRef> result; 164 result.reserve(values.size()); 165 for (const auto& arg : values) { 166 result.push_back(arg.c_str()); 167 } 168 return result; 169 } 170 171 // Transforms a map of {key, value} pairs into a flattened vector of "key=value" 172 // strings. 173 std::vector<std::string> FlattenEnvironment( 174 const absl::flat_hash_map<std::string, std::string>& env) { 175 std::vector<std::string> result; 176 result.reserve(env.size()); 177 for (const auto& entry : env) { 178 result.push_back(absl::StrCat(entry.first, "=", entry.second)); 179 } 180 return result; 181 } 182 183 bool ExecuteAndWait(const std::string& program, 184 const std::vector<std::string>& argv, 185 const std::vector<std::string>& env, 186 const std::string& working_directory) { 187 ScopedWorkingDirectory directory(working_directory); 188 189 std::string error; 190 bool execution_failed = false; 191 int exit_code = llvm::sys::ExecuteAndWait( 192 program, VectorForExecute(argv), VectorForExecute(env), 193 /* Redirects */ std::nullopt, 194 /* SecondsToWait */ 0, /* MemoryLimit */ 0, &error, &execution_failed); 195 if (!error.empty() || execution_failed || exit_code != 0) { 196 LOG(ERROR) << "unable to run extractor (" << exit_code << "): " << error; 197 return false; 198 } 199 return true; 200 } 201 } // namespace 202 203 void CanonicalizeHashes(kpb::CompilationUnit* unit) { 204 HashMap hashes; 205 CanonicalizeHash(&hashes, unit->mutable_entry_context()); 206 for (auto& input : *unit->mutable_required_input()) { 207 for (auto& row : MutableContextRows(&input)) { 208 CanonicalizeHash(&hashes, row.mutable_source_context()); 209 for (auto& column : *row.mutable_column()) { 210 CanonicalizeHash(&hashes, column.mutable_linked_context()); 211 } 212 } 213 } 214 } 215 216 std::optional<std::vector<kpb::CompilationUnit>> ExtractCompilations( 217 ExtractorOptions options) { 218 gpb::LinkMessageReflection<kpb::CxxCompilationUnitDetails>(); 219 220 options.environment.insert({"KYTHE_EXCLUDE_EMPTY_DIRS", "1"}); 221 options.environment.insert({"KYTHE_EXCLUDE_AUTOCONFIGURATION_FILES", "1"}); 222 if (std::optional<std::string> extractor = ResolveRunfiles(kExtractorPath)) { 223 TemporaryKzipFile output_file; 224 options.environment.insert({"KYTHE_OUTPUT_FILE", output_file.filename()}); 225 226 // Add the extractor path as argv[0]. 227 options.arguments.insert(options.arguments.begin(), *extractor); 228 if (ExecuteAndWait(*extractor, options.arguments, 229 FlattenEnvironment(options.environment), 230 options.working_directory)) { 231 if (auto reader = KzipReader::Open(output_file.filename()); reader.ok()) { 232 std::optional<std::vector<kpb::CompilationUnit>> result( 233 absl::in_place); // Default construct a result vector. 234 235 auto status = reader->Scan([&](absl::string_view digest) { 236 if (auto unit = reader->ReadUnit(digest); unit.ok()) { 237 result->push_back(std::move(*unit->mutable_unit())); 238 return true; 239 } else { 240 LOG(ERROR) << "Unable to read CompilationUnit " << digest << ": " 241 << unit.status(); 242 result.reset(); 243 return false; 244 } 245 }); 246 if (!status.ok()) { 247 LOG(ERROR) << "Unable to read compilations: " << status; 248 return std::nullopt; 249 } 250 return result; 251 } else { 252 LOG(ERROR) << "Unable to open " << output_file.filename() << ": " 253 << reader.status(); 254 } 255 return std::nullopt; 256 } 257 258 } else { 259 LOG(ERROR) << "Unable to resolve extractor path"; 260 } 261 return std::nullopt; 262 } 263 264 std::optional<std::string> ResolveRunfiles(absl::string_view path) { 265 std::string error; 266 std::unique_ptr<Runfiles> runfiles(Runfiles::CreateForTest(&error)); 267 if (runfiles == nullptr) { 268 LOG(ERROR) << error; 269 return std::nullopt; 270 } 271 std::string resolved = runfiles->Rlocation(JoinPath(kWorkspaceRoot, path)); 272 if (resolved.empty()) { 273 return std::nullopt; 274 } 275 return resolved; 276 } 277 278 kpb::CompilationUnit ExtractSingleCompilationOrDie(ExtractorOptions options) { 279 if (std::optional<std::vector<kpb::CompilationUnit>> result = 280 ExtractCompilations(std::move(options))) { 281 CHECK(result->size() == 1) 282 << "unexpected number of extracted compilations: " << result->size(); 283 return std::move(result->front()); 284 } else { 285 LOG(FATAL) << "Unable to extract compilation"; 286 } 287 } 288 289 kpb::CompilationUnit ParseTextCompilationUnitOrDie(absl::string_view text) { 290 kpb::CompilationUnit result; 291 CHECK(gpb::TextFormat::ParseFromString(std::string(text), &result)); 292 return result; 293 } 294 } // namespace kythe