brintos

brintos / llvm-project-archived public Read only

0
0
Text · 16.1 KiB · 95bf5e5 Raw
498 lines · cpp
1//===-- ClangdLSPServerTests.cpp ------------------------------------------===//2//3// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.4// See https://llvm.org/LICENSE.txt for license information.5// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception6//7//===----------------------------------------------------------------------===//8 9#include "Annotations.h"10#include "ClangdLSPServer.h"11#include "ClangdServer.h"12#include "ConfigProvider.h"13#include "Diagnostics.h"14#include "Feature.h"15#include "FeatureModule.h"16#include "LSPBinder.h"17#include "LSPClient.h"18#include "TestFS.h"19#include "support/Function.h"20#include "support/Logger.h"21#include "support/TestTracer.h"22#include "support/Threading.h"23#include "clang/Basic/Diagnostic.h"24#include "clang/Basic/LLVM.h"25#include "llvm/ADT/FunctionExtras.h"26#include "llvm/ADT/StringRef.h"27#include "llvm/Support/Error.h"28#include "llvm/Support/FormatVariadic.h"29#include "llvm/Support/JSON.h"30#include "llvm/Support/raw_ostream.h"31#include "llvm/Testing/Support/Error.h"32#include "llvm/Testing/Support/SupportHelpers.h"33#include "gmock/gmock.h"34#include "gtest/gtest.h"35#include <cassert>36#include <condition_variable>37#include <cstddef>38#include <deque>39#include <memory>40#include <mutex>41#include <optional>42#include <thread>43#include <utility>44 45namespace clang {46namespace clangd {47namespace {48using testing::ElementsAre;49 50MATCHER_P(diagMessage, M, "") {51  if (const auto *O = arg.getAsObject()) {52    if (const auto Msg = O->getString("message"))53      return *Msg == M;54  }55  return false;56}57 58class LSPTest : public ::testing::Test {59protected:60  LSPTest() : LogSession(L) {61    ClangdServer::Options &Base = Opts;62    Base = ClangdServer::optsForTest();63    // This is needed to we can test index-based operations like call hierarchy.64    Base.BuildDynamicSymbolIndex = true;65    Base.FeatureModules = &FeatureModules;66  }67 68  LSPClient &start() {69    EXPECT_FALSE(Server) << "Already initialized";70    Server.emplace(Client.transport(), FS, Opts);71    ServerThread.emplace([&] { EXPECT_TRUE(Server->run()); });72    Client.call("initialize", llvm::json::Object{});73    return Client;74  }75 76  void stop() {77    assert(Server);78    Client.call("shutdown", nullptr);79    Client.notify("exit", nullptr);80    Client.stop();81    ServerThread->join();82    Server.reset();83    ServerThread.reset();84  }85 86  ~LSPTest() {87    if (Server)88      stop();89  }90 91  MockFS FS;92  ClangdLSPServer::Options Opts;93  FeatureModuleSet FeatureModules;94 95private:96  class Logger : public clang::clangd::Logger {97    // Color logs so we can distinguish them from test output.98    void log(Level L, const char *Fmt,99             const llvm::formatv_object_base &Message) override {100      raw_ostream::Colors Color;101      switch (L) {102      case Level::Verbose:103        Color = raw_ostream::BLUE;104        break;105      case Level::Error:106        Color = raw_ostream::RED;107        break;108      default:109        Color = raw_ostream::YELLOW;110        break;111      }112      std::lock_guard<std::mutex> Lock(LogMu);113      (llvm::outs().changeColor(Color) << Message << "\n").resetColor();114    }115    std::mutex LogMu;116  };117 118  Logger L;119  LoggingSession LogSession;120  std::optional<ClangdLSPServer> Server;121  std::optional<std::thread> ServerThread;122  LSPClient Client;123};124 125TEST_F(LSPTest, GoToDefinition) {126  Annotations Code(R"cpp(127    int [[fib]](int n) {128      return n >= 2 ? ^fib(n - 1) + fib(n - 2) : 1;129    }130  )cpp");131  auto &Client = start();132  Client.didOpen("foo.cpp", Code.code());133  auto &Def = Client.call("textDocument/definition",134                          llvm::json::Object{135                              {"textDocument", Client.documentID("foo.cpp")},136                              {"position", Code.point()},137                          });138  llvm::json::Value Want = llvm::json::Array{llvm::json::Object{139      {"uri", Client.uri("foo.cpp")}, {"range", Code.range()}}};140  EXPECT_EQ(Def.takeValue(), Want);141}142 143TEST_F(LSPTest, Diagnostics) {144  auto &Client = start();145  Client.didOpen("foo.cpp", "void main(int, char**);");146  EXPECT_THAT(Client.diagnostics("foo.cpp"),147              llvm::ValueIs(testing::ElementsAre(148                  diagMessage("'main' must return 'int' (fix available)"))));149 150  Client.didChange("foo.cpp", "int x = \"42\";");151  EXPECT_THAT(Client.diagnostics("foo.cpp"),152              llvm::ValueIs(testing::ElementsAre(153                  diagMessage("Cannot initialize a variable of type 'int' with "154                              "an lvalue of type 'const char[3]'"))));155 156  Client.didClose("foo.cpp");157  EXPECT_THAT(Client.diagnostics("foo.cpp"), llvm::ValueIs(testing::IsEmpty()));158}159 160TEST_F(LSPTest, DiagnosticsHeaderSaved) {161  auto &Client = start();162  Client.didOpen("foo.cpp", R"cpp(163    #include "foo.h"164    int x = VAR;165  )cpp");166  EXPECT_THAT(Client.diagnostics("foo.cpp"),167              llvm::ValueIs(testing::ElementsAre(168                  diagMessage("'foo.h' file not found"),169                  diagMessage("Use of undeclared identifier 'VAR'"))));170  // Now create the header.171  FS.Files["foo.h"] = "#define VAR original";172  Client.notify(173      "textDocument/didSave",174      llvm::json::Object{{"textDocument", Client.documentID("foo.h")}});175  EXPECT_THAT(Client.diagnostics("foo.cpp"),176              llvm::ValueIs(testing::ElementsAre(177                  diagMessage("Use of undeclared identifier 'original'"))));178  // Now modify the header from within the "editor".179  FS.Files["foo.h"] = "#define VAR changed";180  Client.notify(181      "textDocument/didSave",182      llvm::json::Object{{"textDocument", Client.documentID("foo.h")}});183  // Foo.cpp should be rebuilt with new diagnostics.184  EXPECT_THAT(Client.diagnostics("foo.cpp"),185              llvm::ValueIs(testing::ElementsAre(186                  diagMessage("Use of undeclared identifier 'changed'"))));187}188 189TEST_F(LSPTest, RecordsLatencies) {190  trace::TestTracer Tracer;191  auto &Client = start();192  llvm::StringLiteral MethodName = "method_name";193  EXPECT_THAT(Tracer.takeMetric("lsp_latency", MethodName), testing::SizeIs(0));194  llvm::consumeError(Client.call(MethodName, {}).take().takeError());195  stop();196  EXPECT_THAT(Tracer.takeMetric("lsp_latency", MethodName), testing::SizeIs(1));197}198 199// clang-tidy's renames are converted to clangd's internal rename functionality,200// see clangd#1589 and clangd#741201TEST_F(LSPTest, ClangTidyRename) {202  // This test requires clang-tidy checks to be linked in.203  if (!CLANGD_TIDY_CHECKS)204    return;205  Annotations Header(R"cpp(206    void [[foo]]();207  )cpp");208  Annotations Source(R"cpp(209    void [[foo]]() {}210  )cpp");211  constexpr auto ClangTidyProvider = [](tidy::ClangTidyOptions &ClangTidyOpts,212                                        llvm::StringRef) {213    ClangTidyOpts.Checks = {"-*,readability-identifier-naming"};214    ClangTidyOpts.CheckOptions["readability-identifier-naming.FunctionCase"] =215        "CamelCase";216  };217  Opts.ClangTidyProvider = ClangTidyProvider;218  auto &Client = start();219  Client.didOpen("foo.hpp", Header.code());220  Client.didOpen("foo.cpp", Source.code());221 222  auto Diags = Client.diagnostics("foo.cpp");223  ASSERT_TRUE(Diags && !Diags->empty());224  auto RenameDiag = Diags->front();225 226  auto RenameCommand =227      (*Client228            .call("textDocument/codeAction",229                  llvm::json::Object{230                      {"textDocument", Client.documentID("foo.cpp")},231                      {"context",232                       llvm::json::Object{233                           {"diagnostics", llvm::json::Array{RenameDiag}}}},234                      {"range", Source.range()}})235            .takeValue()236            .getAsArray())[0];237 238  ASSERT_EQ((*RenameCommand.getAsObject())["title"],239            "Apply fix: change 'foo' to 'Foo'");240 241  Client.expectServerCall("workspace/applyEdit");242  Client.call("workspace/executeCommand", RenameCommand);243  Client.sync();244 245  auto Params = Client.takeCallParams("workspace/applyEdit");246  auto Uri = [&](llvm::StringRef Path) {247    return Client.uri(Path).getAsString().value().str();248  };249  llvm::json::Object ExpectedEdit = llvm::json::Object{250      {"edit", llvm::json::Object{251                   {"changes",252                    llvm::json::Object{253                        {Uri("foo.hpp"), llvm::json::Array{llvm::json::Object{254                                             {"range", Header.range()},255                                             {"newText", "Foo"},256                                         }}},257 258                        {Uri("foo.cpp"), llvm::json::Array{llvm::json::Object{259                                             {"range", Source.range()},260                                             {"newText", "Foo"},261                                         }}}262 263                    }}}}};264  EXPECT_EQ(Params, std::vector{llvm::json::Value(std::move(ExpectedEdit))});265}266 267TEST_F(LSPTest, ClangTidyCrash_Issue109367) {268  // This test requires clang-tidy checks to be linked in.269  if (!CLANGD_TIDY_CHECKS)270    return;271  constexpr auto ClangTidyProvider = [](tidy::ClangTidyOptions &ClangTidyOpts,272                                        llvm::StringRef) {273    ClangTidyOpts.Checks = {"-*,boost-use-ranges"};274  };275  Opts.ClangTidyProvider = ClangTidyProvider;276  // Check that registering the boost-use-ranges checker's matchers277  // on two different threads does not cause a crash.278  auto &Client = start();279  Client.didOpen("a.cpp", "");280  Client.didOpen("b.cpp", "");281  Client.sync();282}283 284TEST_F(LSPTest, IncomingCalls) {285  Annotations Code(R"cpp(286    void calle^e(int);287    void caller1() {288      [[callee]](42);289    }290  )cpp");291  auto &Client = start();292  Client.didOpen("foo.cpp", Code.code());293  auto Items = Client294                   .call("textDocument/prepareCallHierarchy",295                         llvm::json::Object{296                             {"textDocument", Client.documentID("foo.cpp")},297                             {"position", Code.point()}})298                   .takeValue();299  auto FirstItem = (*Items.getAsArray())[0];300  auto Calls = Client301                   .call("callHierarchy/incomingCalls",302                         llvm::json::Object{{"item", FirstItem}})303                   .takeValue();304  auto FirstCall = *(*Calls.getAsArray())[0].getAsObject();305  EXPECT_EQ(FirstCall["fromRanges"], llvm::json::Value{Code.range()});306  auto From = *FirstCall["from"].getAsObject();307  EXPECT_EQ(From["name"], "caller1");308}309 310TEST_F(LSPTest, CDBConfigIntegration) {311  auto CfgProvider =312      config::Provider::fromAncestorRelativeYAMLFiles(".clangd", FS);313  Opts.ConfigProvider = CfgProvider.get();314 315  // Map bar.cpp to a different compilation database which defines FOO->BAR.316  FS.Files[".clangd"] = R"yaml(317If:318  PathMatch: bar.cpp319CompileFlags:320  CompilationDatabase: bar321)yaml";322  FS.Files["bar/compile_flags.txt"] = "-DFOO=BAR";323 324  auto &Client = start();325  // foo.cpp gets parsed as normal.326  Client.didOpen("foo.cpp", "int x = FOO;");327  EXPECT_THAT(Client.diagnostics("foo.cpp"),328              llvm::ValueIs(testing::ElementsAre(329                  diagMessage("Use of undeclared identifier 'FOO'"))));330  // bar.cpp shows the configured compile command.331  Client.didOpen("bar.cpp", "int x = FOO;");332  EXPECT_THAT(Client.diagnostics("bar.cpp"),333              llvm::ValueIs(testing::ElementsAre(334                  diagMessage("Use of undeclared identifier 'BAR'"))));335}336 337TEST_F(LSPTest, ModulesTest) {338  class MathModule final : public FeatureModule {339    OutgoingNotification<int> Changed;340    void initializeLSP(LSPBinder &Bind, const llvm::json::Object &ClientCaps,341                       llvm::json::Object &ServerCaps) override {342      Bind.notification("add", this, &MathModule::add);343      Bind.method("get", this, &MathModule::get);344      Changed = Bind.outgoingNotification("changed");345    }346 347    int Value = 0;348 349    void add(const int &X) {350      Value += X;351      Changed(Value);352    }353    void get(const std::nullptr_t &, Callback<int> Reply) {354      scheduler().runQuick(355          "get", "",356          [Reply(std::move(Reply)), Value(Value)]() mutable { Reply(Value); });357    }358  };359  FeatureModules.add(std::make_unique<MathModule>());360 361  auto &Client = start();362  Client.notify("add", 2);363  Client.notify("add", 8);364  EXPECT_EQ(10, Client.call("get", nullptr).takeValue());365  EXPECT_THAT(Client.takeNotifications("changed"),366              ElementsAre(llvm::json::Value(2), llvm::json::Value(10)));367}368 369// Creates a Callback that writes its received value into an370// std::optional<Expected>.371template <typename T>372llvm::unique_function<void(llvm::Expected<T>)>373capture(std::optional<llvm::Expected<T>> &Out) {374  Out.reset();375  return [&Out](llvm::Expected<T> V) { Out.emplace(std::move(V)); };376}377 378TEST_F(LSPTest, FeatureModulesThreadingTest) {379  // A feature module that does its work on a background thread, and so380  // exercises the block/shutdown protocol.381  class AsyncCounter final : public FeatureModule {382    bool ShouldStop = false;383    int State = 0;384    std::deque<Callback<int>> Queue; // null = increment, non-null = read.385    std::condition_variable CV;386    std::mutex Mu;387    std::thread Thread;388 389    void run() {390      std::unique_lock<std::mutex> Lock(Mu);391      while (true) {392        CV.wait(Lock, [&] { return ShouldStop || !Queue.empty(); });393        if (ShouldStop) {394          Queue.clear();395          CV.notify_all();396          return;397        }398        Callback<int> &Task = Queue.front();399        if (Task)400          Task(State);401        else402          ++State;403        Queue.pop_front();404        CV.notify_all();405      }406    }407 408    bool blockUntilIdle(Deadline D) override {409      std::unique_lock<std::mutex> Lock(Mu);410      return clangd::wait(Lock, CV, D, [this] { return Queue.empty(); });411    }412 413    void stop() override {414      {415        std::lock_guard<std::mutex> Lock(Mu);416        ShouldStop = true;417      }418      CV.notify_all();419    }420 421  public:422    AsyncCounter() : Thread([this] { run(); }) {}423    ~AsyncCounter() {424      // Verify shutdown sequence was performed.425      // Real modules would not do this, to be robust to no ClangdServer.426      {427        // We still need the lock here, as Queue might be empty when428        // ClangdServer calls blockUntilIdle, but run() might not have returned429        // yet.430        std::lock_guard<std::mutex> Lock(Mu);431        EXPECT_TRUE(ShouldStop) << "ClangdServer should request shutdown";432        EXPECT_EQ(Queue.size(), 0u) << "ClangdServer should block until idle";433      }434      Thread.join();435    }436 437    void initializeLSP(LSPBinder &Bind, const llvm::json::Object &ClientCaps,438                       llvm::json::Object &ServerCaps) override {439      Bind.notification("increment", this, &AsyncCounter::increment);440    }441 442    // Get the current value, bypassing the queue.443    // Used to verify that sync->blockUntilIdle avoids races in tests.444    int getSync() {445      std::lock_guard<std::mutex> Lock(Mu);446      return State;447    }448 449    // Increment the current value asynchronously.450    void increment(const std::nullptr_t &) {451      {452        std::lock_guard<std::mutex> Lock(Mu);453        Queue.push_back(nullptr);454      }455      CV.notify_all();456    }457  };458 459  FeatureModules.add(std::make_unique<AsyncCounter>());460  auto &Client = start();461 462  Client.notify("increment", nullptr);463  Client.notify("increment", nullptr);464  Client.notify("increment", nullptr);465  Client.sync();466  EXPECT_EQ(3, FeatureModules.get<AsyncCounter>()->getSync());467  // Throw some work on the queue to make sure shutdown blocks on it.468  Client.notify("increment", nullptr);469  Client.notify("increment", nullptr);470  Client.notify("increment", nullptr);471  // And immediately shut down. FeatureModule destructor verifies we blocked.472}473 474TEST_F(LSPTest, DiagModuleTest) {475  static constexpr llvm::StringLiteral DiagMsg = "DiagMsg";476  class DiagModule final : public FeatureModule {477    struct DiagHooks : public ASTListener {478      void sawDiagnostic(const clang::Diagnostic &, clangd::Diag &D) override {479        D.Message = DiagMsg.str();480      }481    };482 483  public:484    std::unique_ptr<ASTListener> astListeners() override {485      return std::make_unique<DiagHooks>();486    }487  };488  FeatureModules.add(std::make_unique<DiagModule>());489 490  auto &Client = start();491  Client.didOpen("foo.cpp", "test;");492  EXPECT_THAT(Client.diagnostics("foo.cpp"),493              llvm::ValueIs(testing::ElementsAre(diagMessage(DiagMsg))));494}495} // namespace496} // namespace clangd497} // namespace clang498