//===- unittest/Tooling/StencilTest.cpp -----------------------------------===// // // 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 // //===----------------------------------------------------------------------===// #include "clang/Tooling/Refactoring/Stencil.h" #include "clang/ASTMatchers/ASTMatchers.h" #include "clang/Tooling/FixIt.h" #include "clang/Tooling/Tooling.h" #include "gmock/gmock.h" #include "gtest/gtest.h" using namespace clang; using namespace tooling; using namespace ast_matchers; namespace { using ::testing::AllOf; using ::testing::Eq; using ::testing::HasSubstr; using MatchResult = MatchFinder::MatchResult; using stencil::cat; using stencil::dPrint; using stencil::text; // In tests, we can't directly match on llvm::Expected since its accessors // mutate the object. So, we collapse it to an Optional. static llvm::Optional toOptional(llvm::Expected V) { if (V) return *V; ADD_FAILURE() << "Losing error in conversion to IsSomething: " << llvm::toString(V.takeError()); return llvm::None; } // A very simple matcher for llvm::Optional values. MATCHER_P(IsSomething, ValueMatcher, "") { if (!arg) return false; return ::testing::ExplainMatchResult(ValueMatcher, *arg, result_listener); } // Create a valid translation-unit from a statement. static std::string wrapSnippet(llvm::Twine StatementCode) { return ("auto stencil_test_snippet = []{" + StatementCode + "};").str(); } static DeclarationMatcher wrapMatcher(const StatementMatcher &Matcher) { return varDecl(hasName("stencil_test_snippet"), hasDescendant(compoundStmt(hasAnySubstatement(Matcher)))); } struct TestMatch { // The AST unit from which `result` is built. We bundle it because it backs // the result. Users are not expected to access it. std::unique_ptr AstUnit; // The result to use in the test. References `ast_unit`. MatchResult Result; }; // Matches `Matcher` against the statement `StatementCode` and returns the // result. Handles putting the statement inside a function and modifying the // matcher correspondingly. `Matcher` should match `StatementCode` exactly -- // that is, produce exactly one match. static llvm::Optional matchStmt(llvm::Twine StatementCode, StatementMatcher Matcher) { auto AstUnit = buildASTFromCode(wrapSnippet(StatementCode)); if (AstUnit == nullptr) { ADD_FAILURE() << "AST construction failed"; return llvm::None; } ASTContext &Context = AstUnit->getASTContext(); auto Matches = ast_matchers::match(wrapMatcher(Matcher), Context); // We expect a single, exact match for the statement. if (Matches.size() != 1) { ADD_FAILURE() << "Wrong number of matches: " << Matches.size(); return llvm::None; } return TestMatch{std::move(AstUnit), MatchResult(Matches[0], &Context)}; } class StencilTest : public ::testing::Test { protected: // Verifies that the given stencil fails when evaluated on a valid match // result. Binds a statement to "stmt", a (non-member) ctor-initializer to // "init", an expression to "expr" and a (nameless) declaration to "decl". void testError(const Stencil &Stencil, ::testing::Matcher Matcher) { const std::string Snippet = R"cc( struct A {}; class F : public A { public: F(int) {} }; F(1); )cc"; auto StmtMatch = matchStmt( Snippet, stmt(hasDescendant( cxxConstructExpr( hasDeclaration(decl(hasDescendant(cxxCtorInitializer( isBaseInitializer()) .bind("init"))) .bind("decl"))) .bind("expr"))) .bind("stmt")); ASSERT_TRUE(StmtMatch); if (auto ResultOrErr = Stencil.eval(StmtMatch->Result)) { ADD_FAILURE() << "Expected failure but succeeded: " << *ResultOrErr; } else { auto Err = llvm::handleErrors(ResultOrErr.takeError(), [&Matcher](const llvm::StringError &Err) { EXPECT_THAT(Err.getMessage(), Matcher); }); if (Err) { ADD_FAILURE() << "Unhandled error: " << llvm::toString(std::move(Err)); } } } // Tests failures caused by references to unbound nodes. `unbound_id` is the // id that will cause the failure. void testUnboundNodeError(const Stencil &Stencil, llvm::StringRef UnboundId) { testError(Stencil, AllOf(HasSubstr(UnboundId), HasSubstr("not bound"))); } }; TEST_F(StencilTest, SingleStatement) { StringRef Condition("C"), Then("T"), Else("E"); const std::string Snippet = R"cc( if (true) return 1; else return 0; )cc"; auto StmtMatch = matchStmt( Snippet, ifStmt(hasCondition(expr().bind(Condition)), hasThen(stmt().bind(Then)), hasElse(stmt().bind(Else)))); ASSERT_TRUE(StmtMatch); // Invert the if-then-else. auto Stencil = cat("if (!", node(Condition), ") ", statement(Else), " else ", statement(Then)); EXPECT_THAT(toOptional(Stencil.eval(StmtMatch->Result)), IsSomething(Eq("if (!true) return 0; else return 1;"))); } TEST_F(StencilTest, SingleStatementCallOperator) { StringRef Condition("C"), Then("T"), Else("E"); const std::string Snippet = R"cc( if (true) return 1; else return 0; )cc"; auto StmtMatch = matchStmt( Snippet, ifStmt(hasCondition(expr().bind(Condition)), hasThen(stmt().bind(Then)), hasElse(stmt().bind(Else)))); ASSERT_TRUE(StmtMatch); // Invert the if-then-else. Stencil S = cat("if (!", node(Condition), ") ", statement(Else), " else ", statement(Then)); EXPECT_THAT(toOptional(S(StmtMatch->Result)), IsSomething(Eq("if (!true) return 0; else return 1;"))); } TEST_F(StencilTest, UnboundNode) { const std::string Snippet = R"cc( if (true) return 1; else return 0; )cc"; auto StmtMatch = matchStmt(Snippet, ifStmt(hasCondition(stmt().bind("a1")), hasThen(stmt().bind("a2")))); ASSERT_TRUE(StmtMatch); auto Stencil = cat("if(!", node("a1"), ") ", node("UNBOUND"), ";"); auto ResultOrErr = Stencil.eval(StmtMatch->Result); EXPECT_TRUE(llvm::errorToBool(ResultOrErr.takeError())) << "Expected unbound node, got " << *ResultOrErr; } // Tests that a stencil with a single parameter (`Id`) evaluates to the expected // string, when `Id` is bound to the expression-statement in `Snippet`. void testExpr(StringRef Id, StringRef Snippet, const Stencil &Stencil, StringRef Expected) { auto StmtMatch = matchStmt(Snippet, expr().bind(Id)); ASSERT_TRUE(StmtMatch); EXPECT_THAT(toOptional(Stencil.eval(StmtMatch->Result)), IsSomething(Expected)); } TEST_F(StencilTest, SelectionOp) { StringRef Id = "id"; testExpr(Id, "3;", cat(node(Id)), "3"); } TEST(StencilEqualityTest, Equality) { auto Lhs = cat("foo", dPrint("dprint_id")); auto Rhs = cat("foo", dPrint("dprint_id")); EXPECT_EQ(Lhs, Rhs); } TEST(StencilEqualityTest, InEqualityDifferentOrdering) { auto Lhs = cat("foo", dPrint("node")); auto Rhs = cat(dPrint("node"), "foo"); EXPECT_NE(Lhs, Rhs); } TEST(StencilEqualityTest, InEqualityDifferentSizes) { auto Lhs = cat("foo", dPrint("node"), "bar", "baz"); auto Rhs = cat("foo", dPrint("node"), "bar"); EXPECT_NE(Lhs, Rhs); } // node is opaque and therefore cannot be examined for equality. TEST(StencilEqualityTest, InEqualitySelection) { auto S1 = cat(node("node")); auto S2 = cat(node("node")); EXPECT_NE(S1, S2); } } // namespace