Files
clice/tests/unit/feature/folding_range_tests.cpp
ykiko cc5b25d5c3 refactor: public feature types and snapshot testing infrastructure (#442)
## Summary

- **Public feature types**: Move `SemanticToken`, `FoldingRange`,
`DocumentSymbol`, `InlayHint`, and `HintCategory` from internal `.cpp`
files to `feature.h` as public API types. Each feature now exposes two
overloads: a raw overload returning offset-based types and a protocol
overload that converts to LSP wire-format with explicit
`PositionEncoding`.
- **Snapshot testing**: Add corpus-driven snapshot tests using
`ASSERT_SNAPSHOT_GLOB` for semantic tokens, folding ranges, inlay hints,
document symbols, and TU index. Tests compile real C++ corpus files,
format output as YAML flow mappings, and diff against `.snap.yml`
baselines.
- **Test infrastructure**: Add `compile_file()` to `Tester`,
`yaml_str()` utility, `--corpus-dir` / `--snapshot-dir` CLI options, and
`--verbose` flag for unit tests. Migrate to kotatsu's unified
`kota::zest::Options` API.
- **Toolchain robustness**: Filter unknown cc1 args via
`clang::driver::getDriverOptTable()` to handle system compilers newer
than embedded LLVM.
- **Dependency bump**: Update kotatsu to 7381404 (unified zest Options,
out-param `from_json` API).

## Details

### Feature type changes
All five feature modules (`semantic_tokens`, `folding_ranges`,
`document_symbols`, `inlay_hints`, `document_links`) now follow the same
two-overload pattern. The raw overload returns offset-based structs
suitable for indexing and testing; the protocol overload adds
`PositionEncoding` conversion for LSP responses. `stateful_worker.cpp`
explicitly passes `PositionEncoding::UTF16` at every call site.

### Snapshot tests
Corpus files live in `tests/corpus/` (organized by language construct).
Snapshot baselines live in `tests/snapshots/<feature>/`. Format lambdas
are inlined directly in test bodies — no separate format functions for
single-use formatters. YAML output uses flow mappings (`- { key: value
}`) for compact, diffable baselines.

### cc1 arg filtering
`src/command/toolchain.cpp` now parses the cc1 argument list through
LLVM's driver option table and drops any args classified as
`UnknownClass`. This prevents compilation failures when the system
compiler emits flags that the embedded LLVM version doesn't recognize.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-24 19:36:27 +08:00

466 lines
8.4 KiB
C++

#include <cstdint>
#include <vector>
#include "test/test.h"
#include "test/tester.h"
#include "feature/feature.h"
namespace clice::testing {
namespace {
namespace protocol = kota::ipc::protocol;
TEST_SUITE(folding_range, Tester) {
std::vector<protocol::FoldingRange> ranges;
enum class LegacyKind {
Namespace,
Class,
Enum,
Struct,
Union,
FunctionBody,
LambdaCapture,
FunctionParams,
FunctionCall,
Initializer,
AccessSpecifier,
Region,
};
void run(llvm::StringRef code) {
add_main("main.cpp", code);
ASSERT_TRUE(compile_with_pch());
ranges = feature::folding_ranges(*unit, feature::PositionEncoding::UTF8);
}
auto to_local_range(const protocol::FoldingRange& range) -> LocalSourceRange {
feature::PositionMapper converter(unit->interested_content(), feature::PositionEncoding::UTF8);
auto start = protocol::Position{
.line = range.start_line,
.character = range.start_character.value_or(0),
};
auto end = protocol::Position{
.line = range.end_line,
.character = range.end_character.value_or(0),
};
return LocalSourceRange(*converter.to_offset(start), *converter.to_offset(end));
}
void EXPECT_FOLDING(std::uint32_t index,
llvm::StringRef begin,
llvm::StringRef end,
LegacyKind,
std::source_location = std::source_location::current()) {
auto actual = to_local_range(ranges[index]);
auto begin_point = point(begin, "main.cpp");
auto end_point = point(end, "main.cpp");
ASSERT_EQ(actual.begin, begin_point);
ASSERT_EQ(actual.end, end_point);
}
using enum LegacyKind;
TEST_CASE(Namespace) {
run(R"cpp(
namespace single_line { }
namespace with_nodes $(1){
struct inner $(3){
int x;
}$(4);
}$(2)
namespace strange
$(5){
}$(6)
#define NS_BEGIN namespace ns {
#define NS_END }
$(7)NS_BEGIN
NS_END$(8)
)cpp");
ASSERT_EQ(ranges.size(), 4U);
EXPECT_FOLDING(0, "1", "2", Namespace);
EXPECT_FOLDING(1, "3", "4", Namespace);
EXPECT_FOLDING(2, "5", "6", Namespace);
EXPECT_FOLDING(3, "7", "8", Namespace);
}
TEST_CASE(Enum) {
run(R"cpp(
enum e1 $(1){
A,
B,
C
}$(2);
enum class e2 $(3){
A,
B,
C
}$(4);
enum e3 { D };
)cpp");
ASSERT_EQ(ranges.size(), 2U);
EXPECT_FOLDING(0, "1", "2", Enum);
EXPECT_FOLDING(1, "3", "4", Enum);
}
TEST_CASE(Record) {
run(R"cpp(
struct s1 $(1){
int x;
float y;
}$(2);
struct s2 {};
struct s3;
union u1 $(3){
int x;
float y;
}$(4);
struct u2 $(5){
struct s4 $(7){
}$(8);
}$(6);
void foo() $(9){
struct s5 $(11){
}$(12);
}$(10)
)cpp");
ASSERT_EQ(ranges.size(), 6U);
EXPECT_FOLDING(0, "1", "2", Struct);
EXPECT_FOLDING(1, "3", "4", Union);
EXPECT_FOLDING(2, "5", "6", Struct);
EXPECT_FOLDING(3, "7", "8", Struct);
EXPECT_FOLDING(4, "9", "10", FunctionBody);
EXPECT_FOLDING(5, "11", "12", Struct);
}
TEST_CASE(Method) {
run(R"cpp(
struct s2 $(1){
int x;
float y;
s2() = default;
}$(2);
struct s3;
struct s3 $(3){
void method() $(5){
int x = 0;
}$(6)
void parameter() $(7){
}$(8)
void skip() {};
}$(4);
)cpp");
ASSERT_EQ(ranges.size(), 4U);
EXPECT_FOLDING(0, "1", "2", Struct);
EXPECT_FOLDING(1, "3", "4", Struct);
EXPECT_FOLDING(2, "5", "6", FunctionBody);
EXPECT_FOLDING(3, "7", "8", FunctionBody);
}
TEST_CASE(Lambda) {
run(R"cpp(
auto z = $(1)[
x = 0, y = 1
]$(2) () $(3){
}$(4);
static int array[4];
auto s = $(5)[
x=0,
y = 1,
z = array[
0],
k = -1
]$(6) () $(7){
return;
}$(8);
auto l1 = [] () {};
auto l2 = [] () $(9){
}$(10);
auto l3 = [] () $(11){
return 0;
}$(12);
auto l4 = [] $(13)(
int x1,
int x2
)$(14) {};
)cpp");
ASSERT_EQ(ranges.size(), 7U);
EXPECT_FOLDING(0, "1", "2", LambdaCapture);
EXPECT_FOLDING(1, "3", "4", FunctionBody);
EXPECT_FOLDING(2, "5", "6", LambdaCapture);
EXPECT_FOLDING(3, "7", "8", FunctionBody);
EXPECT_FOLDING(4, "9", "10", FunctionBody);
EXPECT_FOLDING(5, "11", "12", FunctionBody);
EXPECT_FOLDING(6, "13", "14", FunctionBody);
}
TEST_CASE(Function) {
run(R"cpp(
void e() {};
void f $(1)(
)$(2) $(3){
}$(4)
void g $(5)(
int x,
int y = 2
)$(6) $(7){
int z;
}$(8)
void h() $(9){
int x = 0;
}$(10)
void i( ) { };
void j $(11)(
int p1,
int p2,
...
)$(12);
void k() $(13){
}$(14)
)cpp");
ASSERT_EQ(ranges.size(), 7U);
EXPECT_FOLDING(0, "1", "2", FunctionParams);
EXPECT_FOLDING(1, "3", "4", FunctionBody);
EXPECT_FOLDING(2, "5", "6", FunctionParams);
EXPECT_FOLDING(3, "7", "8", FunctionBody);
EXPECT_FOLDING(4, "9", "10", FunctionBody);
EXPECT_FOLDING(5, "11", "12", FunctionParams);
EXPECT_FOLDING(6, "13", "14", FunctionBody);
}
TEST_CASE(FunctionCall) {
run(R"cpp(
int f(int p1, int p2, int p3, int p4, int p5, int p6) { return p1 + p2; }
int main() $(1){
int x = f(1, 2, 3, 4, 5, 6);
int y = f $(2)(
1, 2, 3,
4, 5, 6
)$(3);
return f $(4)(
1, 2, 3,
4, 5, 6
)$(5);
}$(6)
)cpp");
ASSERT_EQ(ranges.size(), 3U);
EXPECT_FOLDING(0, "1", "6", FunctionBody);
EXPECT_FOLDING(1, "2", "3", FunctionCall);
EXPECT_FOLDING(2, "4", "5", FunctionCall);
}
TEST_CASE(CompoundStmt) {
run(R"cpp(
int main() $(1){
$(3){
$(5){
//
}$(6)
$(7){
//
}$(8)
//
}$(4)
return 0;
}$(2)
)cpp");
}
TEST_CASE(InitializeList) {
run(R"cpp(
struct L { int xs[4]; };
L l1 = $(1){
1, 2, 3, 4
}$(2);
L l2 = $(3){
//
//
}$(4);
)cpp");
ASSERT_EQ(ranges.size(), 2U);
EXPECT_FOLDING(0, "1", "2", Initializer);
EXPECT_FOLDING(1, "3", "4", Initializer);
}
TEST_CASE(AccessSpecifier) {
run(R"cpp(
class c1 $(1){
public$(3):
private$(4):
protected$(5):
}$(2);
class c2 $(6){
public$(8):
int x;
private$(9):
float y;
protected$(10):
double z;
}$(7);
#define PUBLIC public:
#define PRIVATE private:
#define PROTECTED protected:
class c3 $(11){
$(13)PUBLIC
int a;
$(15)PRIVATE$(14)
int b;
$(17)PROTECTED$(16)
int c;
}$(12);
)cpp");
EXPECT_FOLDING(0, "1", "2", Class);
EXPECT_FOLDING(1, "3", "4", AccessSpecifier);
EXPECT_FOLDING(2, "4", "5", AccessSpecifier);
EXPECT_FOLDING(3, "5", "2", AccessSpecifier);
EXPECT_FOLDING(4, "6", "7", Class);
EXPECT_FOLDING(5, "8", "9", AccessSpecifier);
EXPECT_FOLDING(6, "9", "10", AccessSpecifier);
EXPECT_FOLDING(7, "10", "7", AccessSpecifier);
EXPECT_FOLDING(8, "11", "12", Class);
EXPECT_FOLDING(9, "13", "14", AccessSpecifier);
EXPECT_FOLDING(10, "15", "16", AccessSpecifier);
EXPECT_FOLDING(11, "17", "12", AccessSpecifier);
}
TEST_CASE(Directive) {
run(R"cpp(
#ifdef M1
#else
#ifdef M2
#endif
#endif
)cpp");
}
TEST_CASE(PragmaRegion) {
run(R"cpp(
$(1)#pragma region level1
$(2)#pragma region level2
$(3)#pragma region level3
#$(4)pragma endregion level3
#$(5)pragma endregion level2
#$(6)pragma endregion level1
#pragma endregion // mismatch region, skipped
#pragma region // mismatch region, skipped
)cpp");
}
TEST_CASE(snapshot) {
ASSERT_SNAPSHOT_GLOB(corpus_dir, "**/*.cpp", [&](std::string_view path) -> std::string {
if(!compile_file(path))
return "COMPILE_ERROR";
auto ranges = feature::folding_ranges(*unit);
feature::PositionMapper mapper(unit->interested_content(), feature::PositionEncoding::UTF8);
std::string result;
for(auto& r: ranges) {
auto start = mapper.to_position(r.range.begin);
auto end = mapper.to_position(r.range.end);
if(!start || !end)
continue;
result += std::format("- {{ range: \"{}:{}-{}:{}\"",
start->line,
start->character,
end->line,
end->character);
if(r.kind.has_value()) {
result += std::format(", kind: {}", static_cast<const std::string&>(*r.kind));
}
if(!r.collapsed_text.empty()) {
result += std::format(", collapsed_text: {}", yaml_str(r.collapsed_text));
}
result += " }\n";
}
return result;
});
}
}; // TEST_SUITE(folding_range)
} // namespace
} // namespace clice::testing