Files
clice/tests/unit/test/tester.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

383 lines
10 KiB
C++

#include "test/tester.h"
#include <cassert>
#include <format>
#include "syntax/scan.h"
namespace clice::testing {
namespace {
std::vector<std::string> base_cc1_args(llvm::StringRef standard) {
return {
"clang",
"-cc1",
"-triple",
LLVM_DEFAULT_TARGET_TRIPLE,
standard.str(),
"-ffreestanding",
"-undef",
"-fms-extensions",
"-fsyntax-only",
"-x",
"c++",
};
}
} // namespace
Tester::~Tester() {
for(auto& path: pcm_paths) {
fs::remove(path);
}
}
bool Tester::try_compile() {
auto built = clice::compile(params);
if(!built.completed()) {
for(auto& diag: built.diagnostics()) {
LOG_ERROR("{}", diag.message);
}
return false;
}
unit.emplace(std::move(built));
return true;
}
void Tester::prepare(llvm::StringRef standard) {
params = CompilationParams();
unit.reset();
vfs = llvm::makeIntrusiveRefCnt<TestVFS>();
for(auto& [file, source]: sources.all_files) {
vfs->add(file, source.content);
}
owned_args = base_cc1_args(standard);
owned_args.push_back(TestVFS::path(src_path));
params.arguments.clear();
for(auto& arg: owned_args) {
params.arguments.push_back(arg.c_str());
}
params.kind = CompilationKind::Content;
params.vfs = vfs;
}
bool Tester::compile(llvm::StringRef standard) {
prepare(standard);
return try_compile();
}
bool Tester::compile_with_pch(llvm::StringRef standard) {
prepare(standard);
auto pch_path = fs::createTemporaryFile("clice", "pch");
if(!pch_path) {
LOG_ERROR("{}", pch_path.error().message());
return false;
}
auto overlay =
llvm::makeIntrusiveRefCnt<llvm::vfs::OverlayFileSystem>(llvm::vfs::getRealFileSystem());
overlay->pushOverlay(vfs);
params.vfs = overlay;
// Phase 1: Build PCH from the preamble portion.
params.kind = CompilationKind::Preamble;
params.output_file = *pch_path;
auto& main_source = sources.all_files[src_path];
auto bound = compute_preamble_bound(main_source.content);
auto main_vfs_path = TestVFS::path(src_path);
params.add_remapped_file(main_vfs_path, main_source.content, bound);
PCHInfo info;
{
auto preamble_unit = clice::compile(params, info);
if(!preamble_unit.completed()) {
for(auto& diag: preamble_unit.diagnostics()) {
LOG_ERROR("{}", diag.message);
}
return false;
}
}
// Phase 2: Compile content using the PCH.
params.output_file.clear();
params.kind = CompilationKind::Content;
params.pch = {info.path, static_cast<std::uint32_t>(info.preamble.size())};
params.buffers.clear();
return try_compile();
}
bool Tester::compile_with_modules(llvm::StringRef standard) {
std::vector<ModuleFile> all_modules = module_files;
for(auto& [file, source]: sources.all_files) {
if(file == src_path) {
continue;
}
auto result = scan(source.content);
if(!result.module_name.empty() || result.need_preprocess) {
all_modules.push_back({file.str(), source.content});
}
}
if(all_modules.empty()) {
return compile(standard);
}
vfs = llvm::makeIntrusiveRefCnt<TestVFS>();
for(auto& [file, source]: sources.all_files) {
vfs->add(file, source.content);
}
for(auto& mod: module_files) {
vfs->add(mod.filename, mod.content);
}
struct ScannedModule {
std::string filename;
std::string content;
std::string module_name;
std::vector<std::string> deps;
};
auto scan_args_base = base_cc1_args(standard);
std::vector<ScannedModule> modules;
for(auto& mod: all_modules) {
auto args = scan_args_base;
args.push_back(TestVFS::path(mod.filename));
std::vector<const char*> argv;
for(auto& arg: args) {
argv.push_back(arg.c_str());
}
auto result = scan_precise(argv, TestVFS::root(), {}, nullptr, vfs);
modules.push_back(
{mod.filename, mod.content, result.module_name, std::move(result.modules)});
}
llvm::StringMap<std::size_t> name_to_index;
for(std::size_t i = 0; i < modules.size(); ++i) {
name_to_index[modules[i].module_name] = i;
}
std::vector<std::size_t> order;
std::vector<int> state(modules.size(), 0);
auto topo_visit = [&](this auto& self, std::size_t i) -> bool {
if(state[i] == 2)
return true;
if(state[i] == 1) {
LOG_ERROR("Circular module dependency involving {}", modules[i].module_name);
return false;
}
state[i] = 1;
for(auto& dep: modules[i].deps) {
auto it = name_to_index.find(dep);
if(it != name_to_index.end()) {
if(!self(it->second))
return false;
}
}
state[i] = 2;
order.push_back(i);
return true;
};
for(std::size_t i = 0; i < modules.size(); ++i) {
if(!topo_visit(i))
return false;
}
auto overlay =
llvm::makeIntrusiveRefCnt<llvm::vfs::OverlayFileSystem>(llvm::vfs::getRealFileSystem());
overlay->pushOverlay(vfs);
llvm::StringMap<std::string> built_pcms;
for(auto idx: order) {
auto& mod = modules[idx];
auto pcm_path = fs::createTemporaryFile("clice", "pcm");
if(!pcm_path) {
LOG_ERROR("{}", pcm_path.error().message());
return false;
}
pcm_paths.push_back(*pcm_path);
Tester builder;
builder.add_main(mod.filename, mod.content);
builder.prepare(standard);
builder.params.kind = CompilationKind::ModuleInterface;
builder.params.output_file = *pcm_path;
builder.params.vfs = overlay;
builder.params.pcms = built_pcms;
if(!builder.try_compile())
return false;
built_pcms.try_emplace(mod.module_name, *pcm_path);
}
prepare(standard);
params.vfs = overlay;
params.pcms = std::move(built_pcms);
return try_compile();
}
bool Tester::compile_file(llvm::StringRef path, llvm::StringRef standard) {
auto buffer = llvm::MemoryBuffer::getFile(path);
if(!buffer) {
LOG_ERROR("Failed to read file: {}", path);
return false;
}
auto filename = llvm::sys::path::filename(path);
add_main(filename, (*buffer)->getBuffer());
return compile(standard);
}
std::uint32_t Tester::point(llvm::StringRef name, llvm::StringRef file) {
if(file.empty()) {
file = src_path;
}
auto& offsets = sources.all_files[file].offsets;
if(name.empty()) {
assert(offsets.size() == 1);
return offsets.begin()->second;
}
assert(offsets.contains(name));
return offsets.lookup(name);
}
llvm::ArrayRef<std::uint32_t> Tester::nameless_points(llvm::StringRef file) {
if(file.empty()) {
file = src_path;
}
return sources.all_files[file].nameless_offsets;
}
LocalSourceRange Tester::range(llvm::StringRef name, llvm::StringRef file) {
if(file.empty()) {
file = src_path;
}
auto& ranges = sources.all_files[file].ranges;
if(name.empty()) {
assert(ranges.size() == 1);
return ranges.begin()->second;
}
assert(ranges.contains(name));
return ranges.lookup(name);
}
void Tester::prepare_driver(llvm::StringRef standard) {
params = CompilationParams();
unit.reset();
vfs = llvm::makeIntrusiveRefCnt<TestVFS>();
for(auto& [file, source]: sources.all_files) {
vfs->add(file, source.content);
}
auto command = std::format("clang++ {} {} -fms-extensions", standard, src_path);
database.add_command("fake", src_path, command);
CommandOptions options;
options.query_toolchain = true;
options.suppress_logging = true;
auto commands = database.lookup(src_path, options);
assert(!commands.empty() && "lookup failed after add_command");
params.arguments = commands.front().to_argv();
params.kind = CompilationKind::Content;
auto overlay =
llvm::makeIntrusiveRefCnt<llvm::vfs::OverlayFileSystem>(llvm::vfs::getRealFileSystem());
overlay->pushOverlay(vfs);
params.vfs = overlay;
for(auto& [file, source]: sources.all_files) {
if(file == src_path) {
params.add_remapped_file(file, source.content);
} else {
std::string path = path::is_absolute(file) ? file.str() : path::join(".", file);
params.add_remapped_file(path, source.content);
}
}
}
bool Tester::compile_driver(llvm::StringRef standard) {
prepare_driver(standard);
return try_compile();
}
bool Tester::compile_driver_with_pch(llvm::StringRef standard) {
prepare_driver(standard);
auto pch_path = fs::createTemporaryFile("clice", "pch");
if(!pch_path) {
LOG_ERROR("{}", pch_path.error().message());
return false;
}
// Phase 1: Build PCH from the preamble portion.
params.kind = CompilationKind::Preamble;
params.output_file = *pch_path;
// Clear buffers from prepare_driver() so we can re-add with preamble bound.
params.buffers.clear();
for(auto& [file, source]: sources.all_files) {
if(file == src_path) {
auto bound = compute_preamble_bound(source.content);
params.add_remapped_file(file, source.content, bound);
} else {
std::string path = path::is_absolute(file) ? file.str() : path::join(".", file);
params.add_remapped_file(path, source.content);
}
}
PCHInfo info;
{
auto preamble_unit = clice::compile(params, info);
if(!preamble_unit.completed()) {
for(auto& diag: preamble_unit.diagnostics()) {
LOG_ERROR("{}", diag.message);
}
return false;
}
}
// Phase 2: Compile content using the PCH.
params.output_file.clear();
params.kind = CompilationKind::Content;
params.pch = {info.path, static_cast<std::uint32_t>(info.preamble.size())};
params.buffers.clear();
return try_compile();
}
void Tester::clear() {
params = CompilationParams();
database = CompilationDatabase();
unit.reset();
sources.all_files.clear();
src_path.clear();
owned_args.clear();
vfs.reset();
module_files.clear();
for(auto& path: pcm_paths) {
fs::remove(path);
}
pcm_paths.clear();
}
} // namespace clice::testing