//===--- TweakTesting.h - Test helpers for refactoring actions ---*- C++-*-===// // // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. // See https://llvm.org/LICENSE.txt for license information. // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception // //===----------------------------------------------------------------------===// #ifndef LLVM_CLANG_TOOLS_EXTRA_CLANGD_UNITTESTS_TWEAKS_TWEAKTESTING_H #define LLVM_CLANG_TOOLS_EXTRA_CLANGD_UNITTESTS_TWEAKS_TWEAKTESTING_H #include "ParsedAST.h" #include "TestWorkspace.h" #include "index/Index.h" #include "llvm/ADT/StringMap.h" #include "llvm/ADT/StringRef.h" #include "llvm/Testing/Annotations/Annotations.h" #include "gmock/gmock.h" #include "gtest/gtest.h" #include #include namespace clang { namespace clangd { // Fixture base for testing tweaks. Intended to be subclassed for each tweak. // // Usage: // TWEAK_TEST(ExpandDeducedType); // // TEST_F(ExpandDeducedTypeTest, ShortensTypes) { // Header = R"cpp( // namespace foo { template class X{}; } // using namespace foo; // )cpp"; // Context = Function; // EXPECT_THAT(apply("[[auto]] X = foo();"), // "foo X = foo();"); // EXPECT_UNAVAILABLE("auto ^X^ = ^foo();"); // } class TweakTest : public ::testing::Test { const char *TweakID; public: // Inputs are wrapped in file boilerplate before attempting to apply a tweak. // Context describes the type of boilerplate. enum CodeContext { // Code snippet is placed directly into the source file. e.g. a declaration. File, // Snippet will appear within a function body. e.g. a statement. Function, // Snippet is an expression. Expression, }; // Mapping from file name to contents. llvm::StringMap ExtraFiles; protected: TweakTest(const char *TweakID) : TweakID(TweakID) {} // Contents of a header file to be implicitly included. // This typically contains declarations that will be used for a set of related // testcases. std::string Header; llvm::StringRef FileName = "TestTU.cpp"; // Extra flags passed to the compilation in apply(). std::vector ExtraArgs; // Context in which snippets of code should be placed to run tweaks. CodeContext Context = File; // Index to be passed into Tweak::Selection. std::unique_ptr Index = nullptr; // Apply the current tweak to the range (or point) in MarkedCode. // MarkedCode will be wrapped according to the Context. // - if the tweak produces edits, returns the edited code (without markings) // for the main file. // Populates \p EditedFiles if there were changes to other files whenever // it is non-null. It is a mapping from absolute path of the edited file to // its new contents. Passing a nullptr to \p EditedFiles when there are // changes, will result in a failure. // The context added to MarkedCode will be stripped away before returning, // unless the tweak edited it. // - if the tweak produces a message, returns "message:\n" // - if prepare() returns false, returns "unavailable" // - if apply() returns an error, returns "fail: " std::string apply(llvm::StringRef MarkedCode, llvm::StringMap *EditedFiles = nullptr) const; // Helpers for EXPECT_AVAILABLE/EXPECT_UNAVAILABLE macros. using WrappedAST = std::pair; WrappedAST build(llvm::StringRef) const; bool isAvailable(WrappedAST &, llvm::Annotations::Range) const; // Return code re-decorated with a single point/range. static std::string decorate(llvm::StringRef, unsigned); static std::string decorate(llvm::StringRef, llvm::Annotations::Range); }; MATCHER_P2(FileWithContents, FileName, Contents, "") { return arg.first() == FileName && arg.second == Contents; } #define TWEAK_TEST(TweakID) \ class TweakID##Test : public ::clang::clangd::TweakTest { \ protected: \ TweakID##Test() : TweakTest(#TweakID) {} \ } #define EXPECT_AVAILABLE_(MarkedCode, Available) \ do { \ llvm::Annotations A{llvm::StringRef(MarkedCode)}; \ auto AST = build(A.code()); \ assert(!A.points().empty() || !A.ranges().empty()); \ for (const auto &P : A.points()) \ EXPECT_EQ(Available, isAvailable(AST, {P, P})) << decorate(A.code(), P); \ for (const auto &R : A.ranges()) \ EXPECT_EQ(Available, isAvailable(AST, R)) << decorate(A.code(), R); \ } while (0) #define EXPECT_AVAILABLE(MarkedCode) EXPECT_AVAILABLE_(MarkedCode, true) #define EXPECT_UNAVAILABLE(MarkedCode) EXPECT_AVAILABLE_(MarkedCode, false) // A helper class to represent the return value of TweakWorkspaceTest::apply(). struct TweakResult { // A string representation the status of the operation. // For failure cases, this is the same as the return value of // TweakTest::apply() (see the comment above that for details). // For success cases, this is "success". std::string Status; // The contents of all files changed by the tweak, including // the file in which it was invoked. Keys are absolute paths. llvm::StringMap EditedFiles = {}; }; // GTest matchers to allow more easily writing assertions about the // expected value of a TweakResult. MATCHER_P(withStatus, S, "") { return arg.Status == S; } template ::testing::Matcher editedFiles(EditedFilesMatcher M) { return ::testing::Field(&TweakResult::EditedFiles, M); } // Used for formatting TweakResult objects in assertion failure messages, // so it's easier to understand what didn't match. inline llvm::raw_ostream &operator<<(llvm::raw_ostream &Stream, const TweakResult &Result) { Stream << "{ status: " << Result.Status << ", editedFiles: ["; for (const auto &F : Result.EditedFiles) { Stream << F.first() << ":\n"; Stream << F.second; } return Stream << "] }"; } // A version of TweakTest that makes it easier to create test cases that // involve multiple files which are indexed. // Usage: // - Call `Workspace.addMainFile(filename, contents)` to add // source files which are indexer entry points (e.g. would show // up in `compile_commands.json`). // - Call `Workspace.addSource(filename, contents)` to add other // source files (e.g. header files). // - Call `apply(filename, range)` to invoke the tweak on the // indicated file with the given range selected. Can be called // multiple times for the same set of added files. // The implementation takes care of building an index reflecting // all added source files, and making it available to the tweak. // Unlike TweakTest, this does not have a notion of a `CodeContext` // (i.e. the contents of all added files are interpreted as being // in a File context). class TweakWorkspaceTest : public ::testing::Test { const char *TweakID; public: TweakWorkspaceTest(const char *TweakID) : TweakID(TweakID) {} TweakResult apply(StringRef InvocationFile, llvm::Annotations::Range InvocationRange); protected: TestWorkspace Workspace; }; #define TWEAK_WORKSPACE_TEST(TweakID) \ class TweakID##WorkspaceTest : public ::clang::clangd::TweakWorkspaceTest { \ protected: \ TweakID##WorkspaceTest() : TweakWorkspaceTest(#TweakID) {} \ } } // namespace clangd } // namespace clang #endif