
This allows the tests to be run against any implementation of `Solver` instead of begin specific to `WatchedLiteralsSolver` as they currently are.
384 lines
11 KiB
C++
384 lines
11 KiB
C++
//===--- SolverTest.h - Type-parameterized test for solvers ---------------===//
|
|
//
|
|
// 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_ANALYSIS_FLOW_SENSITIVE_SOLVER_TEST_H_
|
|
#define LLVM_CLANG_ANALYSIS_FLOW_SENSITIVE_SOLVER_TEST_H_
|
|
|
|
#include "TestingSupport.h"
|
|
#include "clang/Analysis/FlowSensitive/Solver.h"
|
|
#include "gmock/gmock.h"
|
|
#include "gtest/gtest.h"
|
|
|
|
namespace clang::dataflow::test {
|
|
|
|
namespace {
|
|
|
|
constexpr auto AssignedTrue = Solver::Result::Assignment::AssignedTrue;
|
|
constexpr auto AssignedFalse = Solver::Result::Assignment::AssignedFalse;
|
|
|
|
using testing::_;
|
|
using testing::AnyOf;
|
|
using testing::Pair;
|
|
using testing::UnorderedElementsAre;
|
|
|
|
} // namespace
|
|
|
|
/// Type-parameterized test for implementations of the `Solver` interface.
|
|
/// To use:
|
|
/// 1. Implement a specialization of `createSolverWithLowTimeout()` for the
|
|
/// solver you want to test.
|
|
/// 2. Instantiate the test suite for the solver you want to test using
|
|
/// `INSTANTIATE_TYPED_TEST_SUITE_P()`.
|
|
/// See WatchedLiteralsSolverTest.cpp for an example.
|
|
template <typename SolverT> class SolverTest : public ::testing::Test {
|
|
protected:
|
|
// Checks if the conjunction of `Vals` is satisfiable and returns the
|
|
// corresponding result.
|
|
Solver::Result solve(llvm::ArrayRef<const Formula *> Vals) {
|
|
return SolverT().solve(Vals);
|
|
}
|
|
|
|
// Create a specialization for the solver type to test.
|
|
SolverT createSolverWithLowTimeout();
|
|
};
|
|
|
|
TYPED_TEST_SUITE_P(SolverTest);
|
|
|
|
MATCHER(unsat, "") {
|
|
return arg.getStatus() == Solver::Result::Status::Unsatisfiable;
|
|
}
|
|
|
|
MATCHER_P(sat, SolutionMatcher,
|
|
"is satisfiable, where solution " +
|
|
(testing::DescribeMatcher<
|
|
llvm::DenseMap<Atom, Solver::Result::Assignment>>(
|
|
SolutionMatcher))) {
|
|
if (arg.getStatus() != Solver::Result::Status::Satisfiable)
|
|
return false;
|
|
auto Solution = *arg.getSolution();
|
|
return testing::ExplainMatchResult(SolutionMatcher, Solution,
|
|
result_listener);
|
|
}
|
|
|
|
TYPED_TEST_P(SolverTest, Var) {
|
|
ConstraintContext Ctx;
|
|
auto X = Ctx.atom();
|
|
|
|
// X
|
|
EXPECT_THAT(this->solve({X}),
|
|
sat(UnorderedElementsAre(Pair(X->getAtom(), AssignedTrue))));
|
|
}
|
|
|
|
TYPED_TEST_P(SolverTest, NegatedVar) {
|
|
ConstraintContext Ctx;
|
|
auto X = Ctx.atom();
|
|
auto NotX = Ctx.neg(X);
|
|
|
|
// !X
|
|
EXPECT_THAT(this->solve({NotX}),
|
|
sat(UnorderedElementsAre(Pair(X->getAtom(), AssignedFalse))));
|
|
}
|
|
|
|
TYPED_TEST_P(SolverTest, UnitConflict) {
|
|
ConstraintContext Ctx;
|
|
auto X = Ctx.atom();
|
|
auto NotX = Ctx.neg(X);
|
|
|
|
// X ^ !X
|
|
EXPECT_THAT(this->solve({X, NotX}), unsat());
|
|
}
|
|
|
|
TYPED_TEST_P(SolverTest, DistinctVars) {
|
|
ConstraintContext Ctx;
|
|
auto X = Ctx.atom();
|
|
auto Y = Ctx.atom();
|
|
auto NotY = Ctx.neg(Y);
|
|
|
|
// X ^ !Y
|
|
EXPECT_THAT(this->solve({X, NotY}),
|
|
sat(UnorderedElementsAre(Pair(X->getAtom(), AssignedTrue),
|
|
Pair(Y->getAtom(), AssignedFalse))));
|
|
}
|
|
|
|
TYPED_TEST_P(SolverTest, DoubleNegation) {
|
|
ConstraintContext Ctx;
|
|
auto X = Ctx.atom();
|
|
auto NotX = Ctx.neg(X);
|
|
auto NotNotX = Ctx.neg(NotX);
|
|
|
|
// !!X ^ !X
|
|
EXPECT_THAT(this->solve({NotNotX, NotX}), unsat());
|
|
}
|
|
|
|
TYPED_TEST_P(SolverTest, NegatedDisjunction) {
|
|
ConstraintContext Ctx;
|
|
auto X = Ctx.atom();
|
|
auto Y = Ctx.atom();
|
|
auto XOrY = Ctx.disj(X, Y);
|
|
auto NotXOrY = Ctx.neg(XOrY);
|
|
|
|
// !(X v Y) ^ (X v Y)
|
|
EXPECT_THAT(this->solve({NotXOrY, XOrY}), unsat());
|
|
}
|
|
|
|
TYPED_TEST_P(SolverTest, NegatedConjunction) {
|
|
ConstraintContext Ctx;
|
|
auto X = Ctx.atom();
|
|
auto Y = Ctx.atom();
|
|
auto XAndY = Ctx.conj(X, Y);
|
|
auto NotXAndY = Ctx.neg(XAndY);
|
|
|
|
// !(X ^ Y) ^ (X ^ Y)
|
|
EXPECT_THAT(this->solve({NotXAndY, XAndY}), unsat());
|
|
}
|
|
|
|
TYPED_TEST_P(SolverTest, DisjunctionSameVarWithNegation) {
|
|
ConstraintContext Ctx;
|
|
auto X = Ctx.atom();
|
|
auto NotX = Ctx.neg(X);
|
|
auto XOrNotX = Ctx.disj(X, NotX);
|
|
|
|
// X v !X
|
|
EXPECT_THAT(this->solve({XOrNotX}), sat(_));
|
|
}
|
|
|
|
TYPED_TEST_P(SolverTest, DisjunctionSameVar) {
|
|
ConstraintContext Ctx;
|
|
auto X = Ctx.atom();
|
|
auto XOrX = Ctx.disj(X, X);
|
|
|
|
// X v X
|
|
EXPECT_THAT(this->solve({XOrX}), sat(_));
|
|
}
|
|
|
|
TYPED_TEST_P(SolverTest, ConjunctionSameVarsConflict) {
|
|
ConstraintContext Ctx;
|
|
auto X = Ctx.atom();
|
|
auto NotX = Ctx.neg(X);
|
|
auto XAndNotX = Ctx.conj(X, NotX);
|
|
|
|
// X ^ !X
|
|
EXPECT_THAT(this->solve({XAndNotX}), unsat());
|
|
}
|
|
|
|
TYPED_TEST_P(SolverTest, ConjunctionSameVar) {
|
|
ConstraintContext Ctx;
|
|
auto X = Ctx.atom();
|
|
auto XAndX = Ctx.conj(X, X);
|
|
|
|
// X ^ X
|
|
EXPECT_THAT(this->solve({XAndX}), sat(_));
|
|
}
|
|
|
|
TYPED_TEST_P(SolverTest, PureVar) {
|
|
ConstraintContext Ctx;
|
|
auto X = Ctx.atom();
|
|
auto Y = Ctx.atom();
|
|
auto NotX = Ctx.neg(X);
|
|
auto NotXOrY = Ctx.disj(NotX, Y);
|
|
auto NotY = Ctx.neg(Y);
|
|
auto NotXOrNotY = Ctx.disj(NotX, NotY);
|
|
|
|
// (!X v Y) ^ (!X v !Y)
|
|
EXPECT_THAT(this->solve({NotXOrY, NotXOrNotY}),
|
|
sat(UnorderedElementsAre(Pair(X->getAtom(), AssignedFalse),
|
|
Pair(Y->getAtom(), _))));
|
|
}
|
|
|
|
TYPED_TEST_P(SolverTest, MustAssumeVarIsFalse) {
|
|
ConstraintContext Ctx;
|
|
auto X = Ctx.atom();
|
|
auto Y = Ctx.atom();
|
|
auto XOrY = Ctx.disj(X, Y);
|
|
auto NotX = Ctx.neg(X);
|
|
auto NotXOrY = Ctx.disj(NotX, Y);
|
|
auto NotY = Ctx.neg(Y);
|
|
auto NotXOrNotY = Ctx.disj(NotX, NotY);
|
|
|
|
// (X v Y) ^ (!X v Y) ^ (!X v !Y)
|
|
EXPECT_THAT(this->solve({XOrY, NotXOrY, NotXOrNotY}),
|
|
sat(UnorderedElementsAre(Pair(X->getAtom(), AssignedFalse),
|
|
Pair(Y->getAtom(), AssignedTrue))));
|
|
}
|
|
|
|
TYPED_TEST_P(SolverTest, DeepConflict) {
|
|
ConstraintContext Ctx;
|
|
auto X = Ctx.atom();
|
|
auto Y = Ctx.atom();
|
|
auto XOrY = Ctx.disj(X, Y);
|
|
auto NotX = Ctx.neg(X);
|
|
auto NotXOrY = Ctx.disj(NotX, Y);
|
|
auto NotY = Ctx.neg(Y);
|
|
auto NotXOrNotY = Ctx.disj(NotX, NotY);
|
|
auto XOrNotY = Ctx.disj(X, NotY);
|
|
|
|
// (X v Y) ^ (!X v Y) ^ (!X v !Y) ^ (X v !Y)
|
|
EXPECT_THAT(this->solve({XOrY, NotXOrY, NotXOrNotY, XOrNotY}), unsat());
|
|
}
|
|
|
|
TYPED_TEST_P(SolverTest, IffIsEquivalentToDNF) {
|
|
ConstraintContext Ctx;
|
|
auto X = Ctx.atom();
|
|
auto Y = Ctx.atom();
|
|
auto NotX = Ctx.neg(X);
|
|
auto NotY = Ctx.neg(Y);
|
|
auto XIffY = Ctx.iff(X, Y);
|
|
auto XIffYDNF = Ctx.disj(Ctx.conj(X, Y), Ctx.conj(NotX, NotY));
|
|
auto NotEquivalent = Ctx.neg(Ctx.iff(XIffY, XIffYDNF));
|
|
|
|
// !((X <=> Y) <=> ((X ^ Y) v (!X ^ !Y)))
|
|
EXPECT_THAT(this->solve({NotEquivalent}), unsat());
|
|
}
|
|
|
|
TYPED_TEST_P(SolverTest, IffSameVars) {
|
|
ConstraintContext Ctx;
|
|
auto X = Ctx.atom();
|
|
auto XEqX = Ctx.iff(X, X);
|
|
|
|
// X <=> X
|
|
EXPECT_THAT(this->solve({XEqX}), sat(_));
|
|
}
|
|
|
|
TYPED_TEST_P(SolverTest, IffDistinctVars) {
|
|
ConstraintContext Ctx;
|
|
auto X = Ctx.atom();
|
|
auto Y = Ctx.atom();
|
|
auto XEqY = Ctx.iff(X, Y);
|
|
|
|
// X <=> Y
|
|
EXPECT_THAT(
|
|
this->solve({XEqY}),
|
|
sat(AnyOf(UnorderedElementsAre(Pair(X->getAtom(), AssignedTrue),
|
|
Pair(Y->getAtom(), AssignedTrue)),
|
|
UnorderedElementsAre(Pair(X->getAtom(), AssignedFalse),
|
|
Pair(Y->getAtom(), AssignedFalse)))));
|
|
}
|
|
|
|
TYPED_TEST_P(SolverTest, IffWithUnits) {
|
|
ConstraintContext Ctx;
|
|
auto X = Ctx.atom();
|
|
auto Y = Ctx.atom();
|
|
auto XEqY = Ctx.iff(X, Y);
|
|
|
|
// (X <=> Y) ^ X ^ Y
|
|
EXPECT_THAT(this->solve({XEqY, X, Y}),
|
|
sat(UnorderedElementsAre(Pair(X->getAtom(), AssignedTrue),
|
|
Pair(Y->getAtom(), AssignedTrue))));
|
|
}
|
|
|
|
TYPED_TEST_P(SolverTest, IffWithUnitsConflict) {
|
|
Arena A;
|
|
auto Constraints = parseFormulas(A, R"(
|
|
(V0 = V1)
|
|
V0
|
|
!V1
|
|
)");
|
|
EXPECT_THAT(this->solve(Constraints), unsat());
|
|
}
|
|
|
|
TYPED_TEST_P(SolverTest, IffTransitiveConflict) {
|
|
Arena A;
|
|
auto Constraints = parseFormulas(A, R"(
|
|
(V0 = V1)
|
|
(V1 = V2)
|
|
V2
|
|
!V0
|
|
)");
|
|
EXPECT_THAT(this->solve(Constraints), unsat());
|
|
}
|
|
|
|
TYPED_TEST_P(SolverTest, DeMorgan) {
|
|
Arena A;
|
|
auto Constraints = parseFormulas(A, R"(
|
|
(!(V0 | V1) = (!V0 & !V1))
|
|
(!(V2 & V3) = (!V2 | !V3))
|
|
)");
|
|
EXPECT_THAT(this->solve(Constraints), sat(_));
|
|
}
|
|
|
|
TYPED_TEST_P(SolverTest, RespectsAdditionalConstraints) {
|
|
Arena A;
|
|
auto Constraints = parseFormulas(A, R"(
|
|
(V0 = V1)
|
|
V0
|
|
!V1
|
|
)");
|
|
EXPECT_THAT(this->solve(Constraints), unsat());
|
|
}
|
|
|
|
TYPED_TEST_P(SolverTest, ImplicationIsEquivalentToDNF) {
|
|
Arena A;
|
|
auto Constraints = parseFormulas(A, R"(
|
|
!((V0 => V1) = (!V0 | V1))
|
|
)");
|
|
EXPECT_THAT(this->solve(Constraints), unsat());
|
|
}
|
|
|
|
TYPED_TEST_P(SolverTest, ImplicationConflict) {
|
|
Arena A;
|
|
auto Constraints = parseFormulas(A, R"(
|
|
(V0 => V1)
|
|
(V0 & !V1)
|
|
)");
|
|
EXPECT_THAT(this->solve(Constraints), unsat());
|
|
}
|
|
|
|
TYPED_TEST_P(SolverTest, ReachedLimitsReflectsTimeouts) {
|
|
Arena A;
|
|
auto Constraints = parseFormulas(A, R"(
|
|
(!(V0 | V1) = (!V0 & !V1))
|
|
(!(V2 & V3) = (!V2 & !V3))
|
|
)");
|
|
TypeParam solver = this->createSolverWithLowTimeout();
|
|
ASSERT_EQ(solver.solve(Constraints).getStatus(),
|
|
Solver::Result::Status::TimedOut);
|
|
EXPECT_TRUE(solver.reachedLimit());
|
|
}
|
|
|
|
TYPED_TEST_P(SolverTest, SimpleButLargeContradiction) {
|
|
// This test ensures that the solver takes a short-cut on known
|
|
// contradictory inputs, without using max_iterations. At the time
|
|
// this test is added, formulas that are easily recognized to be
|
|
// contradictory at CNF construction time would lead to timeout.
|
|
TypeParam solver = this->createSolverWithLowTimeout();
|
|
ConstraintContext Ctx;
|
|
auto first = Ctx.atom();
|
|
auto last = first;
|
|
for (int i = 1; i < 10000; ++i) {
|
|
last = Ctx.conj(last, Ctx.atom());
|
|
}
|
|
last = Ctx.conj(Ctx.neg(first), last);
|
|
ASSERT_EQ(solver.solve({last}).getStatus(),
|
|
Solver::Result::Status::Unsatisfiable);
|
|
EXPECT_FALSE(solver.reachedLimit());
|
|
|
|
first = Ctx.atom();
|
|
last = Ctx.neg(first);
|
|
for (int i = 1; i < 10000; ++i) {
|
|
last = Ctx.conj(last, Ctx.neg(Ctx.atom()));
|
|
}
|
|
last = Ctx.conj(first, last);
|
|
ASSERT_EQ(solver.solve({last}).getStatus(),
|
|
Solver::Result::Status::Unsatisfiable);
|
|
EXPECT_FALSE(solver.reachedLimit());
|
|
}
|
|
|
|
REGISTER_TYPED_TEST_SUITE_P(
|
|
SolverTest, Var, NegatedVar, UnitConflict, DistinctVars, DoubleNegation,
|
|
NegatedDisjunction, NegatedConjunction, DisjunctionSameVarWithNegation,
|
|
DisjunctionSameVar, ConjunctionSameVarsConflict, ConjunctionSameVar,
|
|
PureVar, MustAssumeVarIsFalse, DeepConflict, IffIsEquivalentToDNF,
|
|
IffSameVars, IffDistinctVars, IffWithUnits, IffWithUnitsConflict,
|
|
IffTransitiveConflict, DeMorgan, RespectsAdditionalConstraints,
|
|
ImplicationIsEquivalentToDNF, ImplicationConflict,
|
|
ReachedLimitsReflectsTimeouts, SimpleButLargeContradiction);
|
|
|
|
} // namespace clang::dataflow::test
|
|
|
|
#endif // LLVM_CLANG_ANALYSIS_FLOW_SENSITIVE_TESTING_SUPPORT_H_
|