[Testing] Move clangd::Annotations to llvm testing support

Summary:
Annotations allow writing nice-looking unit test code when one needs
access to locations from the source code, e.g. running code completion
at particular offsets in a file. See comments in Annotations.cpp for
more details on the API.

Also got rid of a duplicate annotations parsing code in clang's code
complete tests.

Reviewers: gribozavr, sammccall

Reviewed By: gribozavr

Subscribers: mgorny, hiraditya, ioeric, MaskRay, jkorous, arphaman, kadircet, jdoerfert, cfe-commits, llvm-commits

Tags: #clang, #llvm

Differential Revision: https://reviews.llvm.org/D59814

llvm-svn: 359179
This commit is contained in:
Ilya Biryukov 2019-04-25 10:08:31 +00:00
parent 45d042ed96
commit 6fae38ec91
9 changed files with 347 additions and 134 deletions

View File

@ -12,74 +12,41 @@
namespace clang {
namespace clangd {
// Crash if the assertion fails, printing the message and testcase.
// More elegant error handling isn't needed for unit tests.
static void require(bool Assertion, const char *Msg, llvm::StringRef Code) {
if (!Assertion) {
llvm::errs() << "Annotated testcase: " << Msg << "\n" << Code << "\n";
llvm_unreachable("Annotated testcase assertion failed!");
}
}
Annotations::Annotations(llvm::StringRef Text) {
auto Here = [this] { return offsetToPosition(Code, Code.size()); };
auto Require = [Text](bool Assertion, const char *Msg) {
require(Assertion, Msg, Text);
};
llvm::Optional<llvm::StringRef> Name;
llvm::SmallVector<std::pair<llvm::StringRef, Position>, 8> OpenRanges;
Code.reserve(Text.size());
while (!Text.empty()) {
if (Text.consume_front("^")) {
Points[Name.getValueOr("")].push_back(Here());
Name = None;
continue;
}
if (Text.consume_front("[[")) {
OpenRanges.emplace_back(Name.getValueOr(""), Here());
Name = None;
continue;
}
Require(!Name, "$name should be followed by ^ or [[");
if (Text.consume_front("]]")) {
Require(!OpenRanges.empty(), "unmatched ]]");
Ranges[OpenRanges.back().first].push_back(
{OpenRanges.back().second, Here()});
OpenRanges.pop_back();
continue;
}
if (Text.consume_front("$")) {
Name = Text.take_while(llvm::isAlnum);
Text = Text.drop_front(Name->size());
continue;
}
Code.push_back(Text.front());
Text = Text.drop_front();
}
Require(!Name, "unterminated $name");
Require(OpenRanges.empty(), "unmatched [[");
}
Position Annotations::point(llvm::StringRef Name) const {
auto I = Points.find(Name);
require(I != Points.end() && I->getValue().size() == 1,
"expected exactly one point", Code);
return I->getValue()[0];
return offsetToPosition(code(), Base::point(Name));
}
std::vector<Position> Annotations::points(llvm::StringRef Name) const {
auto P = Points.lookup(Name);
return {P.begin(), P.end()};
auto Offsets = Base::points(Name);
std::vector<Position> Ps;
Ps.reserve(Offsets.size());
for (size_t O : Offsets)
Ps.push_back(offsetToPosition(code(), O));
return Ps;
}
Range Annotations::range(llvm::StringRef Name) const {
auto I = Ranges.find(Name);
require(I != Ranges.end() && I->getValue().size() == 1,
"expected exactly one range", Code);
return I->getValue()[0];
static clangd::Range toLSPRange(llvm::StringRef Code, Annotations::Range R) {
clangd::Range LSPRange;
LSPRange.start = offsetToPosition(Code, R.Begin);
LSPRange.end = offsetToPosition(Code, R.End);
return LSPRange;
}
std::vector<Range> Annotations::ranges(llvm::StringRef Name) const {
auto R = Ranges.lookup(Name);
return {R.begin(), R.end()};
clangd::Range Annotations::range(llvm::StringRef Name) const {
return toLSPRange(code(), Base::range(Name));
}
std::vector<clangd::Range> Annotations::ranges(llvm::StringRef Name) const {
auto OffsetRanges = Base::ranges(Name);
std::vector<clangd::Range> Rs;
Rs.reserve(OffsetRanges.size());
for (Annotations::Range R : OffsetRanges)
Rs.push_back(toLSPRange(code(), R));
return Rs;
}
} // namespace clangd

View File

@ -5,64 +5,32 @@
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
//
// Annotations lets you mark points and ranges inside source code, for tests:
//
// Annotations Example(R"cpp(
// int complete() { x.pri^ } // ^ indicates a point
// void err() { [["hello" == 42]]; } // [[this is a range]]
// $definition^class Foo{}; // points can be named: "definition"
// $fail[[static_assert(false, "")]] // ranges can be named too: "fail"
// )cpp");
//
// StringRef Code = Example.code(); // annotations stripped.
// std::vector<Position> PP = Example.points(); // all unnamed points
// Position P = Example.point(); // there must be exactly one
// Range R = Example.range("fail"); // find named ranges
//
// Points/ranges are coordinates into `code()` which is stripped of annotations.
//
// Ranges may be nested (and points can be inside ranges), but there's no way
// to define general overlapping ranges.
//
// A clangd-specific version of llvm/Testing/Support/Annotations.h, replaces
// offsets and offset-based ranges with types from the LSP protocol.
//===---------------------------------------------------------------------===//
#ifndef LLVM_CLANG_TOOLS_EXTRA_UNITTESTS_CLANGD_ANNOTATIONS_H
#define LLVM_CLANG_TOOLS_EXTRA_UNITTESTS_CLANGD_ANNOTATIONS_H
#include "Protocol.h"
#include "llvm/ADT/SmallVector.h"
#include "llvm/ADT/StringMap.h"
#include "llvm/ADT/StringRef.h"
#include "llvm/Testing/Support/Annotations.h"
namespace clang {
namespace clangd {
class Annotations {
/// Same as llvm::Annotations, but adjusts functions to LSP-specific types for
/// positions and ranges.
class Annotations : public llvm::Annotations {
using Base = llvm::Annotations;
public:
// Parses the annotations from Text. Crashes if it's malformed.
Annotations(llvm::StringRef Text);
using llvm::Annotations::Annotations;
// The input text with all annotations stripped.
// All points and ranges are relative to this stripped text.
llvm::StringRef code() const { return Code; }
// Returns the position of the point marked by ^ (or $name^) in the text.
// Crashes if there isn't exactly one.
Position point(llvm::StringRef Name = "") const;
// Returns the position of all points marked by ^ (or $name^) in the text.
std::vector<Position> points(llvm::StringRef Name = "") const;
// Returns the location of the range marked by [[ ]] (or $name[[ ]]).
// Crashes if there isn't exactly one.
Range range(llvm::StringRef Name = "") const;
// Returns the location of all ranges marked by [[ ]] (or $name[[ ]]).
std::vector<Range> ranges(llvm::StringRef Name = "") const;
private:
std::string Code;
llvm::StringMap<llvm::SmallVector<Position, 1>> Points;
llvm::StringMap<llvm::SmallVector<Range, 1>> Ranges;
clangd::Range range(llvm::StringRef Name = "") const;
std::vector<clangd::Range> ranges(llvm::StringRef Name = "") const;
};
} // namespace clangd

View File

@ -16,4 +16,5 @@ target_link_libraries(SemaTests
clangSema
clangSerialization
clangTooling
LLVMTestingSupport
)

View File

@ -13,6 +13,7 @@
#include "clang/Sema/Sema.h"
#include "clang/Sema/SemaDiagnostic.h"
#include "clang/Tooling/Tooling.h"
#include "llvm/Testing/Support/Annotations.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include <cstddef>
@ -107,41 +108,18 @@ CompletionContext runCompletion(StringRef Code, size_t Offset) {
return ResultCtx;
}
struct ParsedAnnotations {
std::vector<size_t> Points;
std::string Code;
};
ParsedAnnotations parseAnnotations(StringRef AnnotatedCode) {
ParsedAnnotations R;
while (!AnnotatedCode.empty()) {
size_t NextPoint = AnnotatedCode.find('^');
if (NextPoint == StringRef::npos) {
R.Code += AnnotatedCode;
AnnotatedCode = "";
break;
}
R.Code += AnnotatedCode.substr(0, NextPoint);
R.Points.push_back(R.Code.size());
AnnotatedCode = AnnotatedCode.substr(NextPoint + 1);
}
return R;
}
CompletionContext runCodeCompleteOnCode(StringRef AnnotatedCode) {
ParsedAnnotations P = parseAnnotations(AnnotatedCode);
assert(P.Points.size() == 1 && "expected exactly one annotation point");
return runCompletion(P.Code, P.Points.front());
llvm::Annotations A(AnnotatedCode);
return runCompletion(A.code(), A.point());
}
std::vector<std::string>
collectPreferredTypes(StringRef AnnotatedCode,
std::string *PtrDiffType = nullptr) {
ParsedAnnotations P = parseAnnotations(AnnotatedCode);
llvm::Annotations A(AnnotatedCode);
std::vector<std::string> Types;
for (size_t Point : P.Points) {
auto Results = runCompletion(P.Code, Point);
for (size_t Point : A.points()) {
auto Results = runCompletion(A.code(), Point);
if (PtrDiffType) {
assert(PtrDiffType->empty() || *PtrDiffType == Results.PtrDiffType);
*PtrDiffType = Results.PtrDiffType;

View File

@ -0,0 +1,90 @@
//===--- Annotations.h - Annotated source code for 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
//
//===----------------------------------------------------------------------===//
#ifndef LLVM_TESTING_SUPPORT_ANNOTATIONS_H
#define LLVM_TESTING_SUPPORT_ANNOTATIONS_H
#include "llvm/ADT/SmallVector.h"
#include "llvm/ADT/StringMap.h"
#include "llvm/ADT/StringRef.h"
#include <tuple>
#include <vector>
namespace llvm {
/// Annotations lets you mark points and ranges inside source code, for tests:
///
/// Annotations Example(R"cpp(
/// int complete() { x.pri^ } // ^ indicates a point
/// void err() { [["hello" == 42]]; } // [[this is a range]]
/// $definition^class Foo{}; // points can be named: "definition"
/// $fail[[static_assert(false, "")]] // ranges can be named too: "fail"
/// )cpp");
///
/// StringRef Code = Example.code(); // annotations stripped.
/// std::vector<size_t> PP = Example.points(); // all unnamed points
/// size_t P = Example.point(); // there must be exactly one
/// llvm::Range R = Example.range("fail"); // find named ranges
///
/// Points/ranges are coordinated into `code()` which is stripped of
/// annotations.
///
/// Ranges may be nested (and points can be inside ranges), but there's no way
/// to define general overlapping ranges.
///
/// FIXME: the choice of the marking syntax makes it impossible to represent
/// some of the C++ and Objective C constructs (including common ones
/// like C++ attributes). We can fix this by:
/// 1. introducing an escaping mechanism for the special characters,
/// 2. making characters for marking points and ranges configurable,
/// 3. changing the syntax to something less commonly used,
/// 4. ...
class Annotations {
public:
/// Two offsets pointing to a continuous substring. End is not included, i.e.
/// represents a half-open range.
struct Range {
size_t Begin = 0;
size_t End = 0;
friend bool operator==(const Range &L, const Range &R) {
return std::tie(L.Begin, L.End) == std::tie(R.Begin, R.End);
}
friend bool operator!=(const Range &L, const Range &R) { return !(L == R); }
};
/// Parses the annotations from Text. Crashes if it's malformed.
Annotations(llvm::StringRef Text);
/// The input text with all annotations stripped.
/// All points and ranges are relative to this stripped text.
llvm::StringRef code() const { return Code; }
/// Returns the position of the point marked by ^ (or $name^) in the text.
/// Crashes if there isn't exactly one.
size_t point(llvm::StringRef Name = "") const;
/// Returns the position of all points marked by ^ (or $name^) in the text.
std::vector<size_t> points(llvm::StringRef Name = "") const;
/// Returns the location of the range marked by [[ ]] (or $name[[ ]]).
/// Crashes if there isn't exactly one.
Range range(llvm::StringRef Name = "") const;
/// Returns the location of all ranges marked by [[ ]] (or $name[[ ]]).
std::vector<Range> ranges(llvm::StringRef Name = "") const;
private:
std::string Code;
llvm::StringMap<llvm::SmallVector<size_t, 1>> Points;
llvm::StringMap<llvm::SmallVector<Range, 1>> Ranges;
};
llvm::raw_ostream &operator<<(llvm::raw_ostream &O,
const llvm::Annotations::Range &R);
} // namespace llvm
#endif

View File

@ -0,0 +1,95 @@
//===--- Annotations.cpp - Annotated source code for unit 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 "llvm/Testing/Support/Annotations.h"
#include "llvm/ADT/StringExtras.h"
#include "llvm/Support/FormatVariadic.h"
#include "llvm/Support/raw_ostream.h"
using namespace llvm;
// Crash if the assertion fails, printing the message and testcase.
// More elegant error handling isn't needed for unit tests.
static void require(bool Assertion, const char *Msg, llvm::StringRef Code) {
if (!Assertion) {
llvm::errs() << "Annotated testcase: " << Msg << "\n" << Code << "\n";
llvm_unreachable("Annotated testcase assertion failed!");
}
}
Annotations::Annotations(llvm::StringRef Text) {
auto Require = [Text](bool Assertion, const char *Msg) {
require(Assertion, Msg, Text);
};
llvm::Optional<llvm::StringRef> Name;
llvm::SmallVector<std::pair<llvm::StringRef, size_t>, 8> OpenRanges;
Code.reserve(Text.size());
while (!Text.empty()) {
if (Text.consume_front("^")) {
Points[Name.getValueOr("")].push_back(Code.size());
Name = llvm::None;
continue;
}
if (Text.consume_front("[[")) {
OpenRanges.emplace_back(Name.getValueOr(""), Code.size());
Name = llvm::None;
continue;
}
Require(!Name, "$name should be followed by ^ or [[");
if (Text.consume_front("]]")) {
Require(!OpenRanges.empty(), "unmatched ]]");
Range R;
R.Begin = OpenRanges.back().second;
R.End = Code.size();
Ranges[OpenRanges.back().first].push_back(R);
OpenRanges.pop_back();
continue;
}
if (Text.consume_front("$")) {
Name = Text.take_while(llvm::isAlnum);
Text = Text.drop_front(Name->size());
continue;
}
Code.push_back(Text.front());
Text = Text.drop_front();
}
Require(!Name, "unterminated $name");
Require(OpenRanges.empty(), "unmatched [[");
}
size_t Annotations::point(llvm::StringRef Name) const {
auto I = Points.find(Name);
require(I != Points.end() && I->getValue().size() == 1,
"expected exactly one point", Code);
return I->getValue()[0];
}
std::vector<size_t> Annotations::points(llvm::StringRef Name) const {
auto P = Points.lookup(Name);
return {P.begin(), P.end()};
}
Annotations::Range Annotations::range(llvm::StringRef Name) const {
auto I = Ranges.find(Name);
require(I != Ranges.end() && I->getValue().size() == 1,
"expected exactly one range", Code);
return I->getValue()[0];
}
std::vector<Annotations::Range>
Annotations::ranges(llvm::StringRef Name) const {
auto R = Ranges.lookup(Name);
return {R.begin(), R.end()};
}
llvm::raw_ostream &llvm::operator<<(llvm::raw_ostream &O,
const llvm::Annotations::Range &R) {
return O << llvm::formatv("[{0}, {1})", R.Begin, R.End);
}

View File

@ -2,6 +2,7 @@ add_definitions(-DGTEST_LANG_CXX11=1)
add_definitions(-DGTEST_HAS_TR1_TUPLE=0)
add_llvm_library(LLVMTestingSupport
Annotations.cpp
Error.cpp
SupportHelpers.cpp

View File

@ -0,0 +1,112 @@
//===----- unittests/AnnotationsTest.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 "llvm/Testing/Support/Annotations.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
using ::testing::ElementsAre;
using ::testing::IsEmpty;
namespace {
llvm::Annotations::Range range(size_t Begin, size_t End) {
llvm::Annotations::Range R;
R.Begin = Begin;
R.End = End;
return R;
}
TEST(AnnotationsTest, CleanedCode) {
EXPECT_EQ(llvm::Annotations("foo^bar$nnn[[baz$^[[qux]]]]").code(),
"foobarbazqux");
}
TEST(AnnotationsTest, Points) {
// A single point.
EXPECT_EQ(llvm::Annotations("^ab").point(), 0u);
EXPECT_EQ(llvm::Annotations("a^b").point(), 1u);
EXPECT_EQ(llvm::Annotations("ab^").point(), 2u);
// Multiple points.
EXPECT_THAT(llvm::Annotations("^a^bc^d^").points(),
ElementsAre(0u, 1u, 3u, 4u));
// No points.
EXPECT_THAT(llvm::Annotations("ab[[cd]]").points(), IsEmpty());
// Consecutive points.
EXPECT_THAT(llvm::Annotations("ab^^^cd").points(), ElementsAre(2u, 2u, 2u));
}
TEST(AnnotationsTest, Ranges) {
// A single range.
EXPECT_EQ(llvm::Annotations("[[a]]bc").range(), range(0, 1));
EXPECT_EQ(llvm::Annotations("a[[bc]]d").range(), range(1, 3));
EXPECT_EQ(llvm::Annotations("ab[[cd]]").range(), range(2, 4));
// Empty range.
EXPECT_EQ(llvm::Annotations("[[]]ab").range(), range(0, 0));
EXPECT_EQ(llvm::Annotations("a[[]]b").range(), range(1, 1));
EXPECT_EQ(llvm::Annotations("ab[[]]").range(), range(2, 2));
// Multiple ranges.
EXPECT_THAT(llvm::Annotations("[[a]][[b]]cd[[ef]]ef").ranges(),
ElementsAre(range(0, 1), range(1, 2), range(4, 6)));
// No ranges.
EXPECT_THAT(llvm::Annotations("ab^c^defef").ranges(), IsEmpty());
}
TEST(AnnotationsTest, Nested) {
llvm::Annotations Annotated("a[[f^oo^bar[[b[[a]]z]]]]bcdef");
EXPECT_THAT(Annotated.points(), ElementsAre(2u, 4u));
EXPECT_THAT(Annotated.ranges(),
ElementsAre(range(8, 9), range(7, 10), range(1, 10)));
}
TEST(AnnotationsTest, Named) {
// A single named point or range.
EXPECT_EQ(llvm::Annotations("a$foo^b").point("foo"), 1u);
EXPECT_EQ(llvm::Annotations("a$foo[[b]]cdef").range("foo"), range(1, 2));
// Empty names should also work.
EXPECT_EQ(llvm::Annotations("a$^b").point(""), 1u);
EXPECT_EQ(llvm::Annotations("a$[[b]]cdef").range(""), range(1, 2));
// Multiple named points.
llvm::Annotations Annotated("a$p1^bcd$p2^123$p1^345");
EXPECT_THAT(Annotated.points(), IsEmpty());
EXPECT_THAT(Annotated.points("p1"), ElementsAre(1u, 7u));
EXPECT_EQ(Annotated.point("p2"), 4u);
}
TEST(AnnotationsTest, Errors) {
// Annotations use llvm_unreachable, it will only crash in debug mode.
#ifndef NDEBUG
// point() and range() crash on zero or multiple ranges.
EXPECT_DEATH(llvm::Annotations("ab[[c]]def").point(),
"expected exactly one point");
EXPECT_DEATH(llvm::Annotations("a^b^cdef").point(),
"expected exactly one point");
EXPECT_DEATH(llvm::Annotations("a^bcdef").range(),
"expected exactly one range");
EXPECT_DEATH(llvm::Annotations("a[[b]]c[[d]]ef").range(),
"expected exactly one range");
EXPECT_DEATH(llvm::Annotations("$foo^a$foo^a").point("foo"),
"expected exactly one point");
EXPECT_DEATH(llvm::Annotations("$foo[[a]]bc$foo[[a]]").range("foo"),
"expected exactly one range");
// Parsing failures.
EXPECT_DEATH(llvm::Annotations("ff[[fdfd"), "unmatched \\[\\[");
EXPECT_DEATH(llvm::Annotations("ff[[fdjsfjd]]xxx]]"), "unmatched ]]");
EXPECT_DEATH(llvm::Annotations("ff$fdsfd"), "unterminated \\$name");
#endif
}
} // namespace

View File

@ -5,6 +5,7 @@ set(LLVM_LINK_COMPONENTS
add_llvm_unittest(SupportTests
AlignOfTest.cpp
AllocatorTest.cpp
AnnotationsTest.cpp
ARMAttributeParser.cpp
ArrayRecyclerTest.cpp
BinaryStreamTest.cpp