198 lines · c
1//===--- TweakTesting.h - Test helpers for refactoring actions ---*- C++-*-===//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#ifndef LLVM_CLANG_TOOLS_EXTRA_CLANGD_UNITTESTS_TWEAKS_TWEAKTESTING_H10#define LLVM_CLANG_TOOLS_EXTRA_CLANGD_UNITTESTS_TWEAKS_TWEAKTESTING_H11 12#include "ParsedAST.h"13#include "TestWorkspace.h"14#include "index/Index.h"15#include "llvm/ADT/StringMap.h"16#include "llvm/ADT/StringRef.h"17#include "llvm/Testing/Annotations/Annotations.h"18#include "gmock/gmock.h"19#include "gtest/gtest.h"20#include <memory>21#include <string>22 23namespace clang {24namespace clangd {25 26// Fixture base for testing tweaks. Intended to be subclassed for each tweak.27//28// Usage:29// TWEAK_TEST(ExpandDeducedType);30//31// TEST_F(ExpandDeducedTypeTest, ShortensTypes) {32// Header = R"cpp(33// namespace foo { template<typename> class X{}; }34// using namespace foo;35// )cpp";36// Context = Function;37// EXPECT_THAT(apply("[[auto]] X = foo<int>();"),38// "foo<int> X = foo<int();");39// EXPECT_AVAILABLE("^a^u^t^o^ X = foo<int>();");40// EXPECT_UNAVAILABLE("auto ^X^ = ^foo<int>();");41// }42class TweakTest : public ::testing::Test {43 const char *TweakID;44 45public:46 // Inputs are wrapped in file boilerplate before attempting to apply a tweak.47 // Context describes the type of boilerplate.48 enum CodeContext {49 // Code snippet is placed directly into the source file. e.g. a declaration.50 File,51 // Snippet will appear within a function body. e.g. a statement.52 Function,53 // Snippet is an expression.54 Expression,55 };56 57 // Mapping from file name to contents.58 llvm::StringMap<std::string> ExtraFiles;59 60protected:61 TweakTest(const char *TweakID) : TweakID(TweakID) {}62 63 // Contents of a header file to be implicitly included.64 // This typically contains declarations that will be used for a set of related65 // testcases.66 std::string Header;67 68 llvm::StringRef FileName = "TestTU.cpp";69 70 // Extra flags passed to the compilation in apply().71 std::vector<std::string> ExtraArgs;72 73 // Context in which snippets of code should be placed to run tweaks.74 CodeContext Context = File;75 76 // Index to be passed into Tweak::Selection.77 std::unique_ptr<const SymbolIndex> Index = nullptr;78 79 // Apply the current tweak to the range (or point) in MarkedCode.80 // MarkedCode will be wrapped according to the Context.81 // - if the tweak produces edits, returns the edited code (without markings)82 // for the main file.83 // Populates \p EditedFiles if there were changes to other files whenever84 // it is non-null. It is a mapping from absolute path of the edited file to85 // its new contents. Passing a nullptr to \p EditedFiles when there are86 // changes, will result in a failure.87 // The context added to MarkedCode will be stripped away before returning,88 // unless the tweak edited it.89 // - if the tweak produces a message, returns "message:\n<message>"90 // - if prepare() returns false, returns "unavailable"91 // - if apply() returns an error, returns "fail: <message>"92 std::string apply(llvm::StringRef MarkedCode,93 llvm::StringMap<std::string> *EditedFiles = nullptr) const;94 95 // Helpers for EXPECT_AVAILABLE/EXPECT_UNAVAILABLE macros.96 using WrappedAST = std::pair<ParsedAST, /*WrappingOffset*/ unsigned>;97 WrappedAST build(llvm::StringRef) const;98 bool isAvailable(WrappedAST &, llvm::Annotations::Range) const;99 // Return code re-decorated with a single point/range.100 static std::string decorate(llvm::StringRef, unsigned);101 static std::string decorate(llvm::StringRef, llvm::Annotations::Range);102};103 104MATCHER_P2(FileWithContents, FileName, Contents, "") {105 return arg.first() == FileName && arg.second == Contents;106}107 108#define TWEAK_TEST(TweakID) \109 class TweakID##Test : public ::clang::clangd::TweakTest { \110 protected: \111 TweakID##Test() : TweakTest(#TweakID) {} \112 }113 114#define EXPECT_AVAILABLE_(MarkedCode, Available) \115 do { \116 llvm::Annotations A{llvm::StringRef(MarkedCode)}; \117 auto AST = build(A.code()); \118 assert(!A.points().empty() || !A.ranges().empty()); \119 for (const auto &P : A.points()) \120 EXPECT_EQ(Available, isAvailable(AST, {P, P})) << decorate(A.code(), P); \121 for (const auto &R : A.ranges()) \122 EXPECT_EQ(Available, isAvailable(AST, R)) << decorate(A.code(), R); \123 } while (0)124#define EXPECT_AVAILABLE(MarkedCode) EXPECT_AVAILABLE_(MarkedCode, true)125#define EXPECT_UNAVAILABLE(MarkedCode) EXPECT_AVAILABLE_(MarkedCode, false)126 127// A helper class to represent the return value of TweakWorkspaceTest::apply().128struct TweakResult {129 // A string representation the status of the operation.130 // For failure cases, this is the same as the return value of131 // TweakTest::apply() (see the comment above that for details).132 // For success cases, this is "success".133 std::string Status;134 // The contents of all files changed by the tweak, including135 // the file in which it was invoked. Keys are absolute paths.136 llvm::StringMap<std::string> EditedFiles = {};137};138 139// GTest matchers to allow more easily writing assertions about the140// expected value of a TweakResult.141MATCHER_P(withStatus, S, "") { return arg.Status == S; }142template <class EditedFilesMatcher>143::testing::Matcher<TweakResult> editedFiles(EditedFilesMatcher M) {144 return ::testing::Field(&TweakResult::EditedFiles, M);145}146 147// Used for formatting TweakResult objects in assertion failure messages,148// so it's easier to understand what didn't match.149inline llvm::raw_ostream &operator<<(llvm::raw_ostream &Stream,150 const TweakResult &Result) {151 Stream << "{ status: " << Result.Status << ", editedFiles: [";152 for (const auto &F : Result.EditedFiles) {153 Stream << F.first() << ":\n";154 Stream << F.second;155 }156 return Stream << "] }";157}158 159// A version of TweakTest that makes it easier to create test cases that160// involve multiple files which are indexed.161// Usage:162// - Call `Workspace.addMainFile(filename, contents)` to add163// source files which are indexer entry points (e.g. would show164// up in `compile_commands.json`).165// - Call `Workspace.addSource(filename, contents)` to add other166// source files (e.g. header files).167// - Call `apply(filename, range)` to invoke the tweak on the168// indicated file with the given range selected. Can be called169// multiple times for the same set of added files.170// The implementation takes care of building an index reflecting171// all added source files, and making it available to the tweak.172// Unlike TweakTest, this does not have a notion of a `CodeContext`173// (i.e. the contents of all added files are interpreted as being174// in a File context).175class TweakWorkspaceTest : public ::testing::Test {176 const char *TweakID;177 178public:179 TweakWorkspaceTest(const char *TweakID) : TweakID(TweakID) {}180 181 TweakResult apply(StringRef InvocationFile,182 llvm::Annotations::Range InvocationRange);183 184protected:185 TestWorkspace Workspace;186};187 188#define TWEAK_WORKSPACE_TEST(TweakID) \189 class TweakID##WorkspaceTest : public ::clang::clangd::TweakWorkspaceTest { \190 protected: \191 TweakID##WorkspaceTest() : TweakWorkspaceTest(#TweakID) {} \192 }193 194} // namespace clangd195} // namespace clang196 197#endif198