kythe.io@v0.0.68-0.20240422202219-7225dbc01741/kythe/cxx/extractor/cxx_extractor_test.cc (about) 1 /* 2 * Copyright 2014 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 "cxx_extractor.h" 18 19 #include <unistd.h> 20 21 #include <memory> 22 #include <set> 23 #include <string> 24 #include <system_error> 25 #include <unordered_map> 26 #include <utility> 27 #include <vector> 28 29 #include "absl/log/check.h" 30 #include "absl/log/initialize.h" 31 #include "absl/log/log.h" 32 #include "clang/Basic/FileSystemOptions.h" 33 #include "clang/Tooling/Tooling.h" 34 #include "google/protobuf/stubs/common.h" 35 #include "gtest/gtest.h" 36 #include "kythe/cxx/extractor/cxx_details.h" 37 #include "kythe/cxx/extractor/language.h" 38 #include "kythe/proto/analysis.pb.h" 39 #include "llvm/ADT/IntrusiveRefCntPtr.h" 40 #include "llvm/ADT/SmallString.h" 41 #include "llvm/ADT/StringRef.h" 42 #include "llvm/ADT/Twine.h" 43 #include "llvm/Support/FileSystem.h" 44 #include "llvm/Support/Path.h" 45 46 namespace kythe { 47 namespace { 48 49 class CxxExtractorTest : public testing::Test { 50 protected: 51 CxxExtractorTest() { 52 // We use the real filesystem here so that we can test real filesystem 53 // features, like symlinks. 54 CHECK_EQ(0, 55 llvm::sys::fs::createUniqueDirectory("cxx_extractor_test", root_) 56 .value()); 57 directories_to_remove_.insert(std::string(root_.str())); 58 } 59 60 ~CxxExtractorTest() override { 61 // Do the best we can to clean up the temporary files we've made. 62 std::error_code err; 63 for (const auto& file : files_to_remove_) { 64 err = llvm::sys::fs::remove(file); 65 if (err.value()) { 66 LOG(WARNING) << "Couldn't remove " << file << ": " << err.message(); 67 } 68 } 69 // ::remove refuses to remove non-empty directories and we'd like to avoid 70 // recursive traversal out of fear of being bitten by symlinks. 71 bool progress = true; 72 while (progress) { 73 progress = false; 74 for (const auto& dir : directories_to_remove_) { 75 if (!llvm::sys::fs::remove(llvm::Twine(dir)).value()) { 76 directories_to_remove_.erase(dir); 77 progress = true; 78 break; 79 } 80 } 81 } 82 if (!directories_to_remove_.empty()) { 83 LOG(WARNING) << "Couldn't remove temporary directories."; 84 } 85 } 86 87 /// Creates all the non-existent directories in `path` and destroys them 88 /// when the test completes. Ported from `llvm::sys::fs::create_directories`. 89 void UndoableCreateDirectories(const llvm::Twine& path) { 90 llvm::SmallString<128> path_storage; 91 const auto& path_string = path.toStringRef(path_storage); 92 93 // Be optimistic and try to create the directory 94 std::error_code err = llvm::sys::fs::create_directory(path_string, true); 95 // Optimistically check if we succeeded. 96 if (err != std::errc::no_such_file_or_directory) { 97 ASSERT_EQ(0, err.value()); 98 directories_to_remove_.insert(path.str()); 99 return; 100 } 101 102 // We failed because of a no_such_file_or_directory, try to create the 103 // parent. 104 llvm::StringRef parent = llvm::sys::path::parent_path(path_string); 105 ASSERT_FALSE(parent.empty()); 106 UndoableCreateDirectories(parent); 107 ASSERT_EQ(0, llvm::sys::fs::create_directory(path_string).value()); 108 directories_to_remove_.insert(path_string.str()); 109 } 110 111 /// \param path Absolute path, beginning with / (or B:\ or \\, etc), to the 112 /// file to create. 113 /// \param code Code to write at the file named by `path`. 114 void AddAbsoluteSourceFile(llvm::StringRef path, const std::string& code) { 115 int write_fd; 116 UndoableCreateDirectories(path); 117 ASSERT_EQ(0, llvm::sys::fs::remove(path).value()); 118 ASSERT_EQ(0, llvm::sys::fs::openFileForWrite(path, write_fd, 119 llvm::sys::fs::CD_CreateAlways, 120 llvm::sys::fs::OF_Text) 121 .value()); 122 ASSERT_EQ(code.size(), ::write(write_fd, code.c_str(), code.size())); 123 ASSERT_EQ(0, ::close(write_fd)); 124 files_to_remove_.insert(std::string(path)); 125 } 126 127 /// \brief Creates a link named `from` pointing at `to`. Destroys it 128 /// on exit. 129 /// 130 /// This uses `llvm::sys::fs::create_link`, which does different things 131 /// depending on the platform (symlinks on Linux, something else on Windows) 132 void CreateLink(const llvm::Twine& to, const llvm::Twine& from) { 133 ASSERT_EQ(0, llvm::sys::fs::create_link(to, from).value()); 134 files_to_remove_.insert(from.str()); 135 } 136 137 /// \brief Returns an absolute path from the test temporary directory 138 /// ending with `relative_path`. 139 std::string GetRootedPath(const llvm::Twine& relative_path) { 140 llvm::SmallString<256> appended_path; 141 llvm::sys::path::append(appended_path, llvm::Twine(root_), relative_path); 142 CHECK_EQ(0, llvm::sys::fs::make_absolute(appended_path).value()); 143 return std::string(appended_path.str()); 144 } 145 146 /// \brief Adds a file at relative path `path` with content `code`. 147 void AddSourceFile(const llvm::Twine& path, const std::string& code) { 148 std::string absolute_path = GetRootedPath(path); 149 AddAbsoluteSourceFile(absolute_path, code); 150 } 151 152 /// \brief An `CompilationWriterSink` that only holds protocol buffers in 153 /// memory. 154 class CapturingCompilationWriterSink : public kythe::CompilationWriterSink { 155 public: 156 void OpenIndex(const std::string& unit_hash) override {} 157 void WriteHeader(const kythe::proto::CompilationUnit& header) override { 158 units_.push_back(header); 159 } 160 void WriteFileContent(const kythe::proto::FileData& content) override { 161 data_.push_back(content); 162 } 163 const std::vector<kythe::proto::CompilationUnit>& units() const { 164 return units_; 165 } 166 const std::vector<kythe::proto::FileData>& data() const { return data_; } 167 168 private: 169 std::vector<kythe::proto::CompilationUnit> units_; 170 std::vector<kythe::proto::FileData> data_; 171 }; 172 173 /// \brief An `CompilationWriterSink` that forwards all calls to another sink. 174 class ForwardingCompilationWriterSink : public kythe::CompilationWriterSink { 175 public: 176 explicit ForwardingCompilationWriterSink( 177 kythe::CompilationWriterSink* underlying_sink) 178 : underlying_sink_(underlying_sink) {} 179 void OpenIndex(const std::string& unit_hash) override { 180 underlying_sink_->OpenIndex(unit_hash); 181 } 182 void WriteHeader(const kythe::proto::CompilationUnit& header) override { 183 underlying_sink_->WriteHeader(header); 184 } 185 void WriteFileContent(const kythe::proto::FileData& content) override { 186 underlying_sink_->WriteFileContent(content); 187 } 188 189 private: 190 kythe::CompilationWriterSink* underlying_sink_; 191 }; 192 193 /// \brief Runs the extractor on a given source file, directing its output 194 /// to a particular sink. 195 /// \param source_file Path to the main source file. 196 /// \param arguments Arguments (not including the executable name or main 197 /// source file). 198 /// \param required_inputs Paths to required input files. (TODO(zarko): keep?) 199 /// \param revision Revision ID for this index (TODO(zarko): keep?) 200 /// \param output Output file for the compilation. 201 /// \param sink Where to send the result protobufs. 202 void FillCompilationUnit(const std::string& source_file, 203 const std::vector<std::string>& arguments, 204 const std::vector<std::string>& required_inputs, 205 const int revision, const std::string& output, 206 CapturingCompilationWriterSink* sink) { 207 std::vector<std::string> final_arguments = {"tool", "-o", output, 208 "-fsyntax-only", source_file}; 209 final_arguments.insert(final_arguments.end(), arguments.begin(), 210 arguments.end()); 211 kythe::CompilationWriter index_writer; 212 index_writer.set_root_directory(std::string(root_.str())); 213 index_writer.set_args(final_arguments); 214 kythe::proto::CompilationUnit unit; 215 clang::FileSystemOptions file_system_options; 216 file_system_options.WorkingDir = std::string(root_.str()); 217 llvm::IntrusiveRefCntPtr<clang::FileManager> file_manager( 218 new clang::FileManager(file_system_options)); 219 auto extractor = kythe::NewExtractor( 220 &index_writer, 221 [&index_writer, &sink]( 222 const std::string& main_source_file, 223 const PreprocessorTranscript& transcript, 224 const std::unordered_map<std::string, SourceFile>& source_files, 225 const HeaderSearchInfo* header_search_info, bool had_errors) { 226 index_writer.WriteIndex( 227 supported_language::Language::kCpp, 228 std::make_unique<ForwardingCompilationWriterSink>(sink), 229 main_source_file, transcript, source_files, header_search_info, 230 had_errors); 231 }); 232 clang::tooling::ToolInvocation invocation( 233 final_arguments, std::move(extractor), file_manager.get()); 234 invocation.run(); 235 } 236 237 /// \brief Checks that all the files referenced by the compilation unit 238 /// seen by `sink` have also been written out in `sink`. 239 void VerifyCompilationUnit(const CapturingCompilationWriterSink& sink, 240 const int revision, const std::string& output) { 241 ASSERT_EQ(1, sink.units().size()); 242 // TODO(zarko): use or delete revision and output. 243 const kythe::proto::CompilationUnit& unit = sink.units().front(); 244 for (const auto& file_input : unit.required_input()) { 245 const std::string& path = file_input.info().path(); 246 bool found_data = false; 247 for (const auto& file_data : sink.data()) { 248 if (file_data.info().path() == path) { 249 found_data = true; 250 break; 251 } 252 } 253 EXPECT_TRUE(found_data) << "Missing data for " << path; 254 } 255 } 256 257 void FillAndVerifyCompilationUnit( 258 const std::string& path, const std::vector<std::string>& arguments, 259 const std::vector<std::string>& required_inputs, const int revision, 260 const std::string& output) { 261 // TODO(zarko): Is it useful to preserve the old test suite's 262 // `required_inputs` and `revision`, or should they be eliminated? 263 CapturingCompilationWriterSink sink; 264 FillCompilationUnit(path, arguments, required_inputs, revision, output, 265 &sink); 266 VerifyCompilationUnit(sink, revision, output); 267 } 268 269 void FillAndVerifyCompilationUnit( 270 const std::string& path, const std::vector<std::string>& arguments, 271 const std::vector<std::string>& required_inputs, const int revision) { 272 FillAndVerifyCompilationUnit(path, arguments, required_inputs, revision, 273 "output.o"); 274 } 275 276 void FillAndVerifyCompilationUnit( 277 const std::string& path, const std::vector<std::string>& arguments, 278 const std::vector<std::string>& required_inputs) { 279 FillAndVerifyCompilationUnit(path, arguments, required_inputs, 1); 280 } 281 282 /// Path to a directory for test files. 283 llvm::SmallString<256> root_; 284 /// Files to clean up after the test ends. 285 std::set<std::string> files_to_remove_; 286 /// Directories to clean up after the test ends. 287 std::set<std::string> directories_to_remove_; 288 }; 289 290 TEST_F(CxxExtractorTest, SimpleCase) { 291 AddSourceFile("b.cc", "#include \"b.h\"\nint main() { return 0; }"); 292 AddSourceFile("./b.h", "int x;"); 293 FillAndVerifyCompilationUnit("b.cc", {}, {"./b.h", "b.cc"}); 294 } 295 296 TEST_F(CxxExtractorTest, CanIndexIncludesInSubdirectories) { 297 AddSourceFile("a/b.cc", "#include \"c/b.h\"\nint main() { return 0; }"); 298 AddSourceFile("a/c/b.h", "int x;"); 299 FillAndVerifyCompilationUnit("a/b.cc", {"-Ia"}, {"a/b.cc", "a/c/b.h"}, 5); 300 } 301 302 TEST_F(CxxExtractorTest, StoresAllAccessPatternsForIncludes) { 303 AddSourceFile("./lib/b/b.h", "#include \"../a/a.h\""); 304 AddSourceFile("./lib/a/a.h", "int a();"); 305 AddSourceFile("./lib/b/../a/a.h", "int a();"); 306 AddSourceFile("b.cc", 307 "#include \"lib/b/b.h\"\n" 308 "#include \"lib/a/a.h\"\n" 309 "int main() { return 0; }"); 310 FillAndVerifyCompilationUnit( 311 "b.cc", {"-Ilib/b"}, 312 {"./lib/a/a.h", "./lib/b/../a/a.h", "./lib/b/b.h", "b.cc"}); 313 } 314 315 TEST_F(CxxExtractorTest, StoresSameAccessPatternsForReversedIncludes) { 316 AddSourceFile("./lib/b/b.h", "#include \"../a/a.h\""); 317 AddSourceFile("./lib/a/a.h", "int a();"); 318 AddSourceFile("./lib/b/../a/a.h", "int a();"); 319 AddSourceFile("b.cc", 320 "#include \"lib/a/a.h\"\n" 321 "#include \"lib/b/b.h\"\n" 322 "int main() { return 0; }"); 323 FillAndVerifyCompilationUnit( 324 "b.cc", {"-Ilib/b"}, 325 {"./lib/a/a.h", "./lib/b/../a/a.h", "./lib/b/b.h", "b.cc"}); 326 } 327 328 TEST_F(CxxExtractorTest, SupportsIncludeFilesAccessViaDifferentPaths) { 329 AddSourceFile("lib/a/a.h", "class A;"); 330 AddSourceFile("./lib/a/b.h", "class B;"); 331 AddSourceFile("./lib/a/c.h", "#include \"b.h\""); 332 AddSourceFile("lib/a/z.cc", 333 "#include \"a.h\"\n" 334 "#include \"lib/a/c.h\"\n" 335 "int main() { return 0; }"); 336 FillAndVerifyCompilationUnit( 337 "lib/a/z.cc", {"-I."}, 338 {"./lib/a/b.h", "./lib/a/c.h", "lib/a/a.h", "lib/a/z.cc"}); 339 } 340 341 TEST_F(CxxExtractorTest, SupportsAbsoluteIncludes) { 342 std::string absolute_header = GetRootedPath("/a/a.h"); 343 AddAbsoluteSourceFile(absolute_header, "class A;"); 344 345 AddSourceFile("z.cc", "#include \"" + absolute_header + 346 "\"\n" 347 "int main() { return 0; }"); 348 FillAndVerifyCompilationUnit("z.cc", {"-I."}, 349 {absolute_header.c_str(), "z.cc"}); 350 } 351 352 TEST_F(CxxExtractorTest, SupportsIncludeFilesViaSymlinkedSearchPaths) { 353 AddSourceFile("a/sub/a.h", "class A;"); 354 CreateLink(llvm::Twine(GetRootedPath("a/sub")), 355 llvm::Twine(GetRootedPath("b"))); 356 357 AddSourceFile("c/z.cc", 358 "#include \"a.h\"\n" 359 "#include \"sub/a.h\"\n" 360 "int main() { return 0; }"); 361 FillAndVerifyCompilationUnit("c/z.cc", {"-Ia", "-Ib"}, 362 {"a/sub/a.h", "b/a.h", "c/z.cc"}); 363 } 364 365 TEST_F(CxxExtractorTest, DoesNotBreakForCompilerErrors) { 366 AddSourceFile("b.cc", "#include \"b.h\"\nsome ; & garbage"); 367 AddSourceFile("./b.h", "more garbage"); 368 FillAndVerifyCompilationUnit("b.cc", {}, {"./b.h", "b.cc"}); 369 } 370 371 TEST_F(CxxExtractorTest, DoesNotBreakForMissingIncludes) { 372 AddSourceFile("b.cc", "#include \"D.h\"\nint main() { return 0;}"); 373 AddSourceFile("./b.h", "class A;"); 374 FillAndVerifyCompilationUnit("b.cc", {}, {"./b.h", "b.cc"}); 375 } 376 377 } // anonymous namespace 378 } // namespace kythe 379 380 int main(int argc, char** argv) { 381 GOOGLE_PROTOBUF_VERIFY_VERSION; 382 absl::InitializeLog(); 383 ::testing::InitGoogleTest(&argc, argv); 384 int result = RUN_ALL_TESTS(); 385 return result; 386 }