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  }