diff options
Diffstat (limited to 'clang/unittests/Analysis/LifetimeSafetyTest.cpp')
-rw-r--r-- | clang/unittests/Analysis/LifetimeSafetyTest.cpp | 710 |
1 files changed, 710 insertions, 0 deletions
diff --git a/clang/unittests/Analysis/LifetimeSafetyTest.cpp b/clang/unittests/Analysis/LifetimeSafetyTest.cpp new file mode 100644 index 0000000..a48dc45 --- /dev/null +++ b/clang/unittests/Analysis/LifetimeSafetyTest.cpp @@ -0,0 +1,710 @@ +//===- LifetimeSafetyTest.cpp - Lifetime Safety Tests -*---------- 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 +// +//===----------------------------------------------------------------------===// + +#include "clang/Analysis/Analyses/LifetimeSafety.h" +#include "clang/ASTMatchers/ASTMatchFinder.h" +#include "clang/ASTMatchers/ASTMatchers.h" +#include "clang/Testing/TestAST.h" +#include "llvm/ADT/StringMap.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include <optional> +#include <vector> + +namespace clang::lifetimes::internal { +namespace { + +using namespace ast_matchers; +using ::testing::UnorderedElementsAreArray; + +// A helper class to run the full lifetime analysis on a piece of code +// and provide an interface for querying the results. +class LifetimeTestRunner { +public: + LifetimeTestRunner(llvm::StringRef Code) { + std::string FullCode = R"( + #define POINT(name) void("__lifetime_test_point_" #name) + struct MyObj { ~MyObj() {} int i; }; + )"; + FullCode += Code.str(); + + AST = std::make_unique<clang::TestAST>(FullCode); + ASTCtx = &AST->context(); + + // Find the target function using AST matchers. + auto MatchResult = + match(functionDecl(hasName("target")).bind("target"), *ASTCtx); + auto *FD = selectFirst<FunctionDecl>("target", MatchResult); + if (!FD) { + ADD_FAILURE() << "Test case must have a function named 'target'"; + return; + } + AnalysisCtx = std::make_unique<AnalysisDeclContext>(nullptr, FD); + CFG::BuildOptions &BuildOptions = AnalysisCtx->getCFGBuildOptions(); + BuildOptions.setAllAlwaysAdd(); + BuildOptions.AddImplicitDtors = true; + BuildOptions.AddTemporaryDtors = true; + + // Run the main analysis. + Analysis = std::make_unique<LifetimeSafetyAnalysis>(*AnalysisCtx); + Analysis->run(); + + AnnotationToPointMap = Analysis->getTestPoints(); + } + + LifetimeSafetyAnalysis &getAnalysis() { return *Analysis; } + ASTContext &getASTContext() { return *ASTCtx; } + + ProgramPoint getProgramPoint(llvm::StringRef Annotation) { + auto It = AnnotationToPointMap.find(Annotation); + if (It == AnnotationToPointMap.end()) { + ADD_FAILURE() << "Annotation '" << Annotation << "' not found."; + return nullptr; + } + return It->second; + } + +private: + std::unique_ptr<TestAST> AST; + ASTContext *ASTCtx = nullptr; + std::unique_ptr<AnalysisDeclContext> AnalysisCtx; + std::unique_ptr<LifetimeSafetyAnalysis> Analysis; + llvm::StringMap<ProgramPoint> AnnotationToPointMap; +}; + +// A convenience wrapper that uses the LifetimeSafetyAnalysis public API. +class LifetimeTestHelper { +public: + LifetimeTestHelper(LifetimeTestRunner &Runner) + : Runner(Runner), Analysis(Runner.getAnalysis()) {} + + std::optional<OriginID> getOriginForDecl(llvm::StringRef VarName) { + auto *VD = findDecl<ValueDecl>(VarName); + if (!VD) + return std::nullopt; + auto OID = Analysis.getOriginIDForDecl(VD); + if (!OID) + ADD_FAILURE() << "Origin for '" << VarName << "' not found."; + return OID; + } + + std::optional<LoanID> getLoanForVar(llvm::StringRef VarName) { + auto *VD = findDecl<VarDecl>(VarName); + if (!VD) + return std::nullopt; + std::vector<LoanID> LID = Analysis.getLoanIDForVar(VD); + if (LID.empty()) { + ADD_FAILURE() << "Loan for '" << VarName << "' not found."; + return std::nullopt; + } + // TODO: Support retrieving more than one loans to a var. + if (LID.size() > 1) { + ADD_FAILURE() << "More than 1 loans found for '" << VarName; + return std::nullopt; + } + return LID[0]; + } + + std::optional<LoanSet> getLoansAtPoint(OriginID OID, + llvm::StringRef Annotation) { + ProgramPoint PP = Runner.getProgramPoint(Annotation); + if (!PP) + return std::nullopt; + return Analysis.getLoansAtPoint(OID, PP); + } + + std::optional<LoanSet> getExpiredLoansAtPoint(llvm::StringRef Annotation) { + ProgramPoint PP = Runner.getProgramPoint(Annotation); + if (!PP) + return std::nullopt; + return Analysis.getExpiredLoansAtPoint(PP); + } + +private: + template <typename DeclT> DeclT *findDecl(llvm::StringRef Name) { + auto &Ctx = Runner.getASTContext(); + auto Results = match(valueDecl(hasName(Name)).bind("v"), Ctx); + if (Results.empty()) { + ADD_FAILURE() << "Declaration '" << Name << "' not found in AST."; + return nullptr; + } + return const_cast<DeclT *>(selectFirst<DeclT>("v", Results)); + } + + LifetimeTestRunner &Runner; + LifetimeSafetyAnalysis &Analysis; +}; + +// ========================================================================= // +// GTest Matchers & Fixture +// ========================================================================= // + +// A helper class to represent a set of loans, identified by variable names. +class LoanSetInfo { +public: + LoanSetInfo(const std::vector<std::string> &Vars, LifetimeTestHelper &H) + : LoanVars(Vars), Helper(H) {} + std::vector<std::string> LoanVars; + LifetimeTestHelper &Helper; +}; + +// It holds the name of the origin variable and a reference to the helper. +class OriginInfo { +public: + OriginInfo(llvm::StringRef OriginVar, LifetimeTestHelper &Helper) + : OriginVar(OriginVar), Helper(Helper) {} + llvm::StringRef OriginVar; + LifetimeTestHelper &Helper; +}; + +/// Matcher to verify the set of loans held by an origin at a specific +/// program point. +/// +/// This matcher is intended to be used with an \c OriginInfo object. +/// +/// \param LoanVars A vector of strings, where each string is the name of a +/// variable expected to be the source of a loan. +/// \param Annotation A string identifying the program point (created with +/// POINT()) where the check should be performed. +MATCHER_P2(HasLoansToImpl, LoanVars, Annotation, "") { + const OriginInfo &Info = arg; + std::optional<OriginID> OIDOpt = Info.Helper.getOriginForDecl(Info.OriginVar); + if (!OIDOpt) { + *result_listener << "could not find origin for '" << Info.OriginVar.str() + << "'"; + return false; + } + + std::optional<LoanSet> ActualLoansSetOpt = + Info.Helper.getLoansAtPoint(*OIDOpt, Annotation); + if (!ActualLoansSetOpt) { + *result_listener << "could not get a valid loan set at point '" + << Annotation << "'"; + return false; + } + std::vector<LoanID> ActualLoans(ActualLoansSetOpt->begin(), + ActualLoansSetOpt->end()); + + std::vector<LoanID> ExpectedLoans; + for (const auto &LoanVar : LoanVars) { + std::optional<LoanID> ExpectedLIDOpt = Info.Helper.getLoanForVar(LoanVar); + if (!ExpectedLIDOpt) { + *result_listener << "could not find loan for var '" << LoanVar << "'"; + return false; + } + ExpectedLoans.push_back(*ExpectedLIDOpt); + } + + return ExplainMatchResult(UnorderedElementsAreArray(ExpectedLoans), + ActualLoans, result_listener); +} + +/// Matcher to verify that the complete set of expired loans at a program point +/// matches the expected loan set. +MATCHER_P(AreExpiredAt, Annotation, "") { + const LoanSetInfo &Info = arg; + auto &Helper = Info.Helper; + + auto ActualExpiredSetOpt = Helper.getExpiredLoansAtPoint(Annotation); + if (!ActualExpiredSetOpt) { + *result_listener << "could not get a valid expired loan set at point '" + << Annotation << "'"; + return false; + } + std::vector<LoanID> ActualExpiredLoans(ActualExpiredSetOpt->begin(), + ActualExpiredSetOpt->end()); + std::vector<LoanID> ExpectedExpiredLoans; + for (const auto &VarName : Info.LoanVars) { + auto LoanIDOpt = Helper.getLoanForVar(VarName); + if (!LoanIDOpt) { + *result_listener << "could not find a loan for variable '" << VarName + << "'"; + return false; + } + ExpectedExpiredLoans.push_back(*LoanIDOpt); + } + return ExplainMatchResult(UnorderedElementsAreArray(ExpectedExpiredLoans), + ActualExpiredLoans, result_listener); +} + +// Base test fixture to manage the runner and helper. +class LifetimeAnalysisTest : public ::testing::Test { +protected: + void SetupTest(llvm::StringRef Code) { + Runner = std::make_unique<LifetimeTestRunner>(Code); + Helper = std::make_unique<LifetimeTestHelper>(*Runner); + } + + OriginInfo Origin(llvm::StringRef OriginVar) { + return OriginInfo(OriginVar, *Helper); + } + + /// Factory function that hides the std::vector creation. + LoanSetInfo LoansTo(std::initializer_list<std::string> LoanVars) { + return LoanSetInfo({LoanVars}, *Helper); + } + + /// A convenience helper for asserting that no loans are expired. + LoanSetInfo NoLoans() { return LoansTo({}); } + + // Factory function that hides the std::vector creation. + auto HasLoansTo(std::initializer_list<std::string> LoanVars, + const char *Annotation) { + return HasLoansToImpl(std::vector<std::string>(LoanVars), Annotation); + } + + std::unique_ptr<LifetimeTestRunner> Runner; + std::unique_ptr<LifetimeTestHelper> Helper; +}; + +// ========================================================================= // +// TESTS +// ========================================================================= // + +TEST_F(LifetimeAnalysisTest, SimpleLoanAndOrigin) { + SetupTest(R"( + void target() { + int x; + int* p = &x; + POINT(p1); + } + )"); + EXPECT_THAT(Origin("p"), HasLoansTo({"x"}, "p1")); +} + +TEST_F(LifetimeAnalysisTest, OverwriteOrigin) { + SetupTest(R"( + void target() { + MyObj s1, s2; + + MyObj* p = &s1; + POINT(after_s1); + + p = &s2; + POINT(after_s2); + } + )"); + EXPECT_THAT(Origin("p"), HasLoansTo({"s1"}, "after_s1")); + EXPECT_THAT(Origin("p"), HasLoansTo({"s2"}, "after_s2")); +} + +TEST_F(LifetimeAnalysisTest, ConditionalLoan) { + SetupTest(R"( + void target(bool cond) { + int a, b; + int *p = nullptr; + if (cond) { + p = &a; + POINT(after_then); + } else { + p = &b; + POINT(after_else); + } + POINT(after_if); + } + )"); + EXPECT_THAT(Origin("p"), HasLoansTo({"a"}, "after_then")); + EXPECT_THAT(Origin("p"), HasLoansTo({"b"}, "after_else")); + EXPECT_THAT(Origin("p"), HasLoansTo({"a", "b"}, "after_if")); +} + +TEST_F(LifetimeAnalysisTest, PointerChain) { + SetupTest(R"( + void target() { + MyObj y; + MyObj* ptr1 = &y; + POINT(p1); + + MyObj* ptr2 = ptr1; + POINT(p2); + + ptr2 = ptr1; + POINT(p3); + + ptr2 = ptr2; // Self assignment + POINT(p4); + } + )"); + EXPECT_THAT(Origin("ptr1"), HasLoansTo({"y"}, "p1")); + EXPECT_THAT(Origin("ptr2"), HasLoansTo({"y"}, "p2")); + EXPECT_THAT(Origin("ptr2"), HasLoansTo({"y"}, "p3")); + EXPECT_THAT(Origin("ptr2"), HasLoansTo({"y"}, "p4")); +} + +TEST_F(LifetimeAnalysisTest, ReassignToNull) { + SetupTest(R"( + void target() { + MyObj s1; + MyObj* p = &s1; + POINT(before_null); + p = nullptr; + POINT(after_null); + } + )"); + EXPECT_THAT(Origin("p"), HasLoansTo({"s1"}, "before_null")); + EXPECT_THAT(Origin("p"), HasLoansTo({}, "after_null")); +} + +TEST_F(LifetimeAnalysisTest, ReassignInIf) { + SetupTest(R"( + void target(bool condition) { + MyObj s1, s2; + MyObj* p = &s1; + POINT(before_if); + if (condition) { + p = &s2; + POINT(after_reassign); + } + POINT(after_if); + } + )"); + EXPECT_THAT(Origin("p"), HasLoansTo({"s1"}, "before_if")); + EXPECT_THAT(Origin("p"), HasLoansTo({"s2"}, "after_reassign")); + EXPECT_THAT(Origin("p"), HasLoansTo({"s1", "s2"}, "after_if")); +} + +TEST_F(LifetimeAnalysisTest, AssignInSwitch) { + SetupTest(R"( + void target(int mode) { + MyObj s1, s2, s3; + MyObj* p = nullptr; + switch (mode) { + case 1: + p = &s1; + POINT(case1); + break; + case 2: + p = &s2; + POINT(case2); + break; + default: + p = &s3; + POINT(case3); + break; + } + POINT(after_switch); + } + )"); + EXPECT_THAT(Origin("p"), HasLoansTo({"s1"}, "case1")); + EXPECT_THAT(Origin("p"), HasLoansTo({"s2"}, "case2")); + EXPECT_THAT(Origin("p"), HasLoansTo({"s3"}, "case3")); + EXPECT_THAT(Origin("p"), HasLoansTo({"s1", "s2", "s3"}, "after_switch")); +} + +TEST_F(LifetimeAnalysisTest, LoanInLoop) { + SetupTest(R"( + void target(bool condition) { + MyObj* p = nullptr; + while (condition) { + POINT(start_loop); + MyObj inner; + p = &inner; + POINT(end_loop); + } + POINT(after_loop); + } + )"); + EXPECT_THAT(Origin("p"), HasLoansTo({"inner"}, "start_loop")); + EXPECT_THAT(LoansTo({"inner"}), AreExpiredAt("start_loop")); + + EXPECT_THAT(Origin("p"), HasLoansTo({"inner"}, "end_loop")); + EXPECT_THAT(NoLoans(), AreExpiredAt("end_loop")); + + EXPECT_THAT(Origin("p"), HasLoansTo({"inner"}, "after_loop")); + EXPECT_THAT(LoansTo({"inner"}), AreExpiredAt("after_loop")); +} + +TEST_F(LifetimeAnalysisTest, LoopWithBreak) { + SetupTest(R"( + void target(int count) { + MyObj s1; + MyObj s2; + MyObj* p = &s1; + POINT(before_loop); + for (int i = 0; i < count; ++i) { + if (i == 5) { + p = &s2; + POINT(inside_if); + break; + } + POINT(after_if); + } + POINT(after_loop); + } + )"); + EXPECT_THAT(Origin("p"), HasLoansTo({"s1"}, "before_loop")); + EXPECT_THAT(Origin("p"), HasLoansTo({"s2"}, "inside_if")); + // At the join point after if, s2 cannot make it to p without the if. + EXPECT_THAT(Origin("p"), HasLoansTo({"s1"}, "after_if")); + // At the join point after the loop, p could hold a loan to s1 (if the loop + // completed normally) or to s2 (if the loop was broken). + EXPECT_THAT(Origin("p"), HasLoansTo({"s1", "s2"}, "after_loop")); +} + +TEST_F(LifetimeAnalysisTest, PointersInACycle) { + SetupTest(R"( + void target(bool condition) { + MyObj v1, v2, v3; + MyObj *p1 = &v1, *p2 = &v2, *p3 = &v3; + + POINT(before_while); + while (condition) { + MyObj* temp = p1; + p1 = p2; + p2 = p3; + p3 = temp; + } + POINT(after_loop); + } + )"); + EXPECT_THAT(Origin("p1"), HasLoansTo({"v1"}, "before_while")); + EXPECT_THAT(Origin("p2"), HasLoansTo({"v2"}, "before_while")); + EXPECT_THAT(Origin("p3"), HasLoansTo({"v3"}, "before_while")); + + // At the fixed point after the loop, all pointers could point to any of + // the three variables. + EXPECT_THAT(Origin("p1"), HasLoansTo({"v1", "v2", "v3"}, "after_loop")); + EXPECT_THAT(Origin("p2"), HasLoansTo({"v1", "v2", "v3"}, "after_loop")); + EXPECT_THAT(Origin("p3"), HasLoansTo({"v1", "v2", "v3"}, "after_loop")); + EXPECT_THAT(Origin("temp"), HasLoansTo({"v1", "v2", "v3"}, "after_loop")); +} + +TEST_F(LifetimeAnalysisTest, PointersAndExpirationInACycle) { + SetupTest(R"( + void target(bool condition) { + MyObj v1, v2; + MyObj *p1 = &v1, *p2 = &v2; + + POINT(before_while); + while (condition) { + POINT(in_loop_before_temp); + MyObj temp; + p1 = &temp; + POINT(in_loop_after_temp); + + MyObj* q = p1; + p1 = p2; + p2 = q; + } + POINT(after_loop); + } + )"); + EXPECT_THAT(Origin("p1"), HasLoansTo({"v1"}, "before_while")); + EXPECT_THAT(Origin("p2"), HasLoansTo({"v2"}, "before_while")); + EXPECT_THAT(NoLoans(), AreExpiredAt("before_while")); + + EXPECT_THAT(Origin("p1"), + HasLoansTo({"v1", "v2", "temp"}, "in_loop_before_temp")); + EXPECT_THAT(Origin("p2"), HasLoansTo({"v2", "temp"}, "in_loop_before_temp")); + EXPECT_THAT(LoansTo({"temp"}), AreExpiredAt("in_loop_before_temp")); + + EXPECT_THAT(Origin("p1"), HasLoansTo({"temp"}, "in_loop_after_temp")); + EXPECT_THAT(Origin("p2"), HasLoansTo({"v2", "temp"}, "in_loop_after_temp")); + EXPECT_THAT(NoLoans(), AreExpiredAt("in_loop_after_temp")); + + EXPECT_THAT(Origin("p1"), HasLoansTo({"v1", "v2", "temp"}, "after_loop")); + EXPECT_THAT(Origin("p2"), HasLoansTo({"v2", "temp"}, "after_loop")); + EXPECT_THAT(LoansTo({"temp"}), AreExpiredAt("after_loop")); +} + +TEST_F(LifetimeAnalysisTest, NestedScopes) { + SetupTest(R"( + void target() { + MyObj* p = nullptr; + { + MyObj outer; + p = &outer; + POINT(before_inner_scope); + { + MyObj inner; + p = &inner; + POINT(inside_inner_scope); + } // inner expires + POINT(after_inner_scope); + } // outer expires + } + )"); + EXPECT_THAT(Origin("p"), HasLoansTo({"outer"}, "before_inner_scope")); + EXPECT_THAT(Origin("p"), HasLoansTo({"inner"}, "inside_inner_scope")); + EXPECT_THAT(Origin("p"), HasLoansTo({"inner"}, "after_inner_scope")); +} + +TEST_F(LifetimeAnalysisTest, SimpleExpiry) { + SetupTest(R"( + void target() { + MyObj* p = nullptr; + { + MyObj s; + p = &s; + POINT(before_expiry); + } // s goes out of scope here + POINT(after_expiry); + } + )"); + EXPECT_THAT(NoLoans(), AreExpiredAt("before_expiry")); + EXPECT_THAT(LoansTo({"s"}), AreExpiredAt("after_expiry")); +} + +TEST_F(LifetimeAnalysisTest, NestedExpiry) { + SetupTest(R"( + void target() { + MyObj s1; + MyObj* p = &s1; + POINT(before_inner); + { + MyObj s2; + p = &s2; + POINT(in_inner); + } // s2 expires + POINT(after_inner); + } + )"); + EXPECT_THAT(NoLoans(), AreExpiredAt("before_inner")); + EXPECT_THAT(NoLoans(), AreExpiredAt("in_inner")); + EXPECT_THAT(LoansTo({"s2"}), AreExpiredAt("after_inner")); +} + +TEST_F(LifetimeAnalysisTest, ConditionalExpiry) { + SetupTest(R"( + void target(bool cond) { + MyObj s1; + MyObj* p = &s1; + POINT(before_if); + if (cond) { + MyObj s2; + p = &s2; + POINT(then_block); + } // s2 expires here + POINT(after_if); + } + )"); + EXPECT_THAT(NoLoans(), AreExpiredAt("before_if")); + EXPECT_THAT(NoLoans(), AreExpiredAt("then_block")); + EXPECT_THAT(LoansTo({"s2"}), AreExpiredAt("after_if")); +} + +TEST_F(LifetimeAnalysisTest, LoopExpiry) { + SetupTest(R"( + void target() { + MyObj *p = nullptr; + for (int i = 0; i < 2; ++i) { + POINT(start_loop); + MyObj s; + p = &s; + POINT(end_loop); + } // s expires here on each iteration + POINT(after_loop); + } + )"); + EXPECT_THAT(LoansTo({"s"}), AreExpiredAt("start_loop")); + EXPECT_THAT(NoLoans(), AreExpiredAt("end_loop")); + EXPECT_THAT(LoansTo({"s"}), AreExpiredAt("after_loop")); +} + +TEST_F(LifetimeAnalysisTest, MultipleExpiredLoans) { + SetupTest(R"( + void target() { + MyObj *p1, *p2, *p3; + { + MyObj s1; + p1 = &s1; + POINT(p1); + } // s1 expires + POINT(p2); + { + MyObj s2; + p2 = &s2; + MyObj s3; + p3 = &s3; + POINT(p3); + } // s2, s3 expire + POINT(p4); + } + )"); + EXPECT_THAT(NoLoans(), AreExpiredAt("p1")); + EXPECT_THAT(LoansTo({"s1"}), AreExpiredAt("p2")); + EXPECT_THAT(LoansTo({"s1"}), AreExpiredAt("p3")); + EXPECT_THAT(LoansTo({"s1", "s2", "s3"}), AreExpiredAt("p4")); +} + +TEST_F(LifetimeAnalysisTest, GotoJumpsOutOfScope) { + SetupTest(R"( + void target(bool cond) { + MyObj *p = nullptr; + { + MyObj s; + p = &s; + POINT(before_goto); + if (cond) { + goto end; + } + } // `s` expires here on the path that doesn't jump + POINT(after_scope); + end: + POINT(after_goto); + } + )"); + EXPECT_THAT(NoLoans(), AreExpiredAt("before_goto")); + EXPECT_THAT(LoansTo({"s"}), AreExpiredAt("after_scope")); + EXPECT_THAT(LoansTo({"s"}), AreExpiredAt("after_goto")); +} + +TEST_F(LifetimeAnalysisTest, ContinueInLoop) { + SetupTest(R"( + void target(int count) { + MyObj *p = nullptr; + MyObj outer; + p = &outer; + POINT(before_loop); + + for (int i = 0; i < count; ++i) { + if (i % 2 == 0) { + MyObj s_even; + p = &s_even; + POINT(in_even_iter); + continue; + } + MyObj s_odd; + p = &s_odd; + POINT(in_odd_iter); + } + POINT(after_loop); + } + )"); + EXPECT_THAT(NoLoans(), AreExpiredAt("before_loop")); + EXPECT_THAT(LoansTo({"s_odd"}), AreExpiredAt("in_even_iter")); + EXPECT_THAT(LoansTo({"s_even"}), AreExpiredAt("in_odd_iter")); + EXPECT_THAT(LoansTo({"s_even", "s_odd"}), AreExpiredAt("after_loop")); +} + +TEST_F(LifetimeAnalysisTest, ReassignedPointerThenOriginalExpires) { + SetupTest(R"( + void target() { + MyObj* p = nullptr; + { + MyObj s1; + p = &s1; + POINT(p_has_s1); + { + MyObj s2; + p = &s2; + POINT(p_has_s2); + } + POINT(p_after_s2_expires); + } // s1 expires here. + POINT(p_after_s1_expires); + } + )"); + EXPECT_THAT(NoLoans(), AreExpiredAt("p_has_s1")); + EXPECT_THAT(NoLoans(), AreExpiredAt("p_has_s2")); + EXPECT_THAT(LoansTo({"s2"}), AreExpiredAt("p_after_s2_expires")); + EXPECT_THAT(LoansTo({"s1", "s2"}), AreExpiredAt("p_after_s1_expires")); +} + +} // anonymous namespace +} // namespace clang::lifetimes::internal |