From cc5b25d5c3d86982392ffc9f8029d5875731ea30 Mon Sep 17 00:00:00 2001 From: ykiko Date: Sun, 24 May 2026 19:36:27 +0800 Subject: [PATCH] refactor: public feature types and snapshot testing infrastructure (#442) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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//`. 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 --- cmake/package.cmake | 2 +- pixi.toml | 4 +- src/command/toolchain.cpp | 29 ++++++- src/feature/document_symbols.cpp | 30 ++++---- src/feature/feature.h | 69 +++++++++++++---- src/feature/folding_ranges.cpp | 18 ++--- src/feature/inlay_hints.cpp | 47 +++++------- src/feature/semantic_tokens.cpp | 26 +++---- src/server/worker/stateful_worker.cpp | 17 +++-- src/server/workspace/config.cpp | 6 +- tests/corpus/statements/if/basic_if.cpp | 36 +++++++++ .../statements/if/basic_if.cpp.snap.yml | 14 ++++ .../statements/if/basic_if.cpp.snap.yml | 11 +++ .../statements/if/basic_if.cpp.snap.yml | 11 +++ .../statements/if/basic_if.cpp.snap.yml | 75 +++++++++++++++++++ .../statements/if/basic_if.cpp.snap.yml | 28 +++++++ tests/unit/feature/document_link_tests.cpp | 4 +- tests/unit/feature/document_symbol_tests.cpp | 57 +++++++++++++- tests/unit/feature/folding_range_tests.cpp | 33 +++++++- tests/unit/feature/inlay_hint_tests.cpp | 39 +++++++++- tests/unit/feature/semantic_tokens_tests.cpp | 52 ++++++++++++- tests/unit/index/tu_index_tests.cpp | 67 ++++++++++++++++- tests/unit/test/platform.h | 8 ++ tests/unit/test/tester.cpp | 11 +++ tests/unit/test/tester.h | 28 +++++++ tests/unit/unit_tests.cc | 43 ++++++----- 26 files changed, 638 insertions(+), 127 deletions(-) create mode 100644 tests/corpus/statements/if/basic_if.cpp create mode 100644 tests/snapshots/document_symbol/snapshot/statements/if/basic_if.cpp.snap.yml create mode 100644 tests/snapshots/folding_range/snapshot/statements/if/basic_if.cpp.snap.yml create mode 100644 tests/snapshots/inlay_hint/snapshot/statements/if/basic_if.cpp.snap.yml create mode 100644 tests/snapshots/semantic_tokens/snapshot/statements/if/basic_if.cpp.snap.yml create mode 100644 tests/snapshots/tu_index/snapshot/statements/if/basic_if.cpp.snap.yml diff --git a/cmake/package.cmake b/cmake/package.cmake index 1f7f06da..f3ef7b95 100644 --- a/cmake/package.cmake +++ b/cmake/package.cmake @@ -41,7 +41,7 @@ set(FLATBUFFERS_BUILD_FLATHASH OFF CACHE BOOL "" FORCE) FetchContent_Declare( kotatsu GIT_REPOSITORY https://github.com/clice-io/kotatsu - GIT_TAG e024f3b427a554502c4aa015952800a03ca4384b + GIT_TAG 73814044ce8142f4438a3028f44668675fc09fff ) set(KOTA_ENABLE_ZEST ON) diff --git a/pixi.toml b/pixi.toml index bedbd717..8c05d9e3 100644 --- a/pixi.toml +++ b/pixi.toml @@ -161,7 +161,7 @@ depends-on = [{ task = "lint-cpp", args = ["{{ type }}"] }] [feature.test.tasks.unit-test] args = [{ arg = "type", default = "RelWithDebInfo" }] -cmd = './build/{{ type }}/bin/unit_tests --test-dir="./tests/data"' +cmd = './build/{{ type }}/bin/unit_tests --test-dir="./tests/data" --snapshot-dir="./tests/snapshots" --corpus-dir="./tests/corpus" --verbose' [feature.test.tasks.integration-test] args = [{ arg = "type", default = "RelWithDebInfo" }] @@ -257,7 +257,7 @@ format-markdown = "fd -H -e md -x prettier --write" format-json = "fd -H -e json -E package-lock.json -x prettier --write" format-toml = "fd -H -e toml -x tombi format" format-yaml = """ -fd -H -e yaml -e yml -E pnpm-lock.yaml -x prettier --write && \ +fd -H -e yaml -e yml -E pnpm-lock.yaml -E '*.snap.yml' -x prettier --write && \ fd -H "^\\.clang-(format|tidy)$" -x prettier --write --parser yaml """ format = { depends-on = [ diff --git a/src/command/toolchain.cpp b/src/command/toolchain.cpp index c655337f..b9dbc9ec 100644 --- a/src/command/toolchain.cpp +++ b/src/command/toolchain.cpp @@ -9,6 +9,7 @@ #include "support/logging.h" #include "kota/meta/enum.h" +#include "llvm/ADT/DenseSet.h" #include "llvm/ADT/ScopeExit.h" #include "llvm/Support/CommandLine.h" #include "llvm/Support/FileSystem.h" @@ -17,6 +18,7 @@ #include "llvm/TargetParser/Host.h" #include "clang/Driver/Compilation.h" #include "clang/Driver/Driver.h" +#include "clang/Driver/Options.h" #include "clang/Driver/Tool.h" #ifndef _WIN32 @@ -470,11 +472,32 @@ std::vector query_clang_toolchain(const QueryParams& params) { continue; } - for(auto arg: args) { - if(arg == "-###"sv) { + // FIXME: the system compiler may be newer than our embedded LLVM, + // producing cc1 flags we don't recognize. Filter them out here. + // Long-term we should unify the command pipeline so the driver + // version always matches the embedded LLVM. + auto& table = clang::driver::getDriverOptTable(); + auto cc1_args = llvm::ArrayRef(args).drop_front(2); + unsigned missing_index = 0, missing_count = 0; + auto parsed = table.ParseArgs(cc1_args, missing_index, missing_count); + + llvm::DenseSet unknown_indices; + for(auto* a: parsed) { + if(a->getOption().getKind() == llvm::opt::Option::UnknownClass) { + unknown_indices.insert(a->getIndex()); + } + } + + result.emplace_back(params.callback(args[0])); + result.emplace_back(params.callback(args[1])); + for(unsigned i = 0; i < cc1_args.size(); ++i) { + if(unknown_indices.contains(i)) { continue; } - result.emplace_back(params.callback(arg)); + if(cc1_args[i] == "-###"sv) { + continue; + } + result.emplace_back(params.callback(cc1_args[i])); } } } diff --git a/src/feature/document_symbols.cpp b/src/feature/document_symbols.cpp index a9ca933b..70ddba71 100644 --- a/src/feature/document_symbols.cpp +++ b/src/feature/document_symbols.cpp @@ -93,18 +93,9 @@ auto symbol_detail(clang::ASTContext& context, const clang::NamedDecl& decl) -> return detail; } -struct InternalSymbol { - std::string name; - std::string detail; - SymbolKind kind = SymbolKind::Invalid; - LocalSourceRange range; - LocalSourceRange selection_range; - std::vector children; -}; - struct SymbolFrame { - std::vector symbols; - std::vector* cursor = &symbols; + std::vector symbols; + std::vector* cursor = &symbols; }; class DocumentSymbolCollector : public FilteredASTVisitor { @@ -143,7 +134,7 @@ public: return ok; } - auto collect() -> std::vector { + auto collect() -> std::vector { TraverseDecl(unit.tu()); return std::move(result.symbols); } @@ -174,8 +165,8 @@ private: SymbolFrame result; }; -void sort_symbols(std::vector& symbols) { - std::ranges::sort(symbols, [](const InternalSymbol& lhs, const InternalSymbol& rhs) { +void sort_symbols(std::vector& symbols) { + std::ranges::sort(symbols, [](const DocumentSymbol& lhs, const DocumentSymbol& rhs) { if(lhs.range.begin != rhs.range.begin) { return lhs.range.begin < rhs.range.begin; } @@ -187,7 +178,7 @@ void sort_symbols(std::vector& symbols) { } } -auto to_protocol_symbol(const InternalSymbol& symbol, const PositionMapper& converter) +auto to_protocol_symbol(const DocumentSymbol& symbol, const PositionMapper& converter) -> protocol::DocumentSymbol { protocol::DocumentSymbol result{ .name = symbol.name, @@ -215,10 +206,15 @@ auto to_protocol_symbol(const InternalSymbol& symbol, const PositionMapper& conv } // namespace +auto document_symbols(CompilationUnitRef unit) -> std::vector { + auto result = DocumentSymbolCollector(unit).collect(); + sort_symbols(result); + return result; +} + auto document_symbols(CompilationUnitRef unit, PositionEncoding encoding) -> std::vector { - auto internal = DocumentSymbolCollector(unit).collect(); - sort_symbols(internal); + auto internal = document_symbols(unit); PositionMapper converter(unit.interested_content(), encoding); std::vector symbols; diff --git a/src/feature/feature.h b/src/feature/feature.h index b4904280..3b5f3ad6 100644 --- a/src/feature/feature.h +++ b/src/feature/feature.h @@ -7,6 +7,7 @@ #include "compile/compilation.h" #include "compile/compilation_unit.h" +#include "semantic/symbol_kind.h" #include "kota/ipc/lsp/position.h" #include "kota/ipc/lsp/protocol.h" @@ -59,18 +60,66 @@ struct InlayHintsOptions { struct SignatureHelpOptions {}; -auto semantic_tokens(CompilationUnitRef unit, PositionEncoding encoding = PositionEncoding::UTF16) +struct SemanticToken { + LocalSourceRange range; + SymbolKind kind = SymbolKind::Invalid; + std::uint32_t modifiers = 0; +}; + +struct FoldingRange { + LocalSourceRange range; + std::optional kind; + std::string collapsed_text; +}; + +struct DocumentSymbol { + std::string name; + std::string detail; + SymbolKind kind = SymbolKind::Invalid; + LocalSourceRange range; + LocalSourceRange selection_range; + std::vector children; +}; + +enum class HintCategory : std::uint8_t { + Parameter, + DefaultArgument, + Type, + Designator, + BlockEnd, +}; + +struct InlayHint { + std::uint32_t offset = 0; + HintCategory kind = HintCategory::Type; + std::string label; + bool padding_left = false; + bool padding_right = false; +}; + +auto semantic_tokens(CompilationUnitRef unit) -> std::vector; +auto semantic_tokens(CompilationUnitRef unit, PositionEncoding encoding) -> protocol::SemanticTokens; +auto folding_ranges(CompilationUnitRef unit) -> std::vector; +auto folding_ranges(CompilationUnitRef unit, PositionEncoding encoding) + -> std::vector; + +auto document_symbols(CompilationUnitRef unit) -> std::vector; +auto document_symbols(CompilationUnitRef unit, PositionEncoding encoding) + -> std::vector; + +auto inlay_hints(CompilationUnitRef unit, + LocalSourceRange target, + const InlayHintsOptions& options = {}) -> std::vector; +auto inlay_hints(CompilationUnitRef unit, + LocalSourceRange target, + const InlayHintsOptions& options, + PositionEncoding encoding) -> std::vector; + auto document_links(CompilationUnitRef unit, PositionEncoding encoding = PositionEncoding::UTF16) -> std::vector; -auto document_symbols(CompilationUnitRef unit, PositionEncoding encoding = PositionEncoding::UTF16) - -> std::vector; - -auto folding_ranges(CompilationUnitRef unit, PositionEncoding encoding = PositionEncoding::UTF16) - -> std::vector; - auto diagnostics(CompilationUnitRef unit, PositionEncoding encoding = PositionEncoding::UTF16) -> std::vector; @@ -89,12 +138,6 @@ auto hover(CompilationUnitRef unit, const HoverOptions& options = {}, PositionEncoding encoding = PositionEncoding::UTF16) -> std::optional; -auto inlay_hints(CompilationUnitRef unit, - LocalSourceRange target, - const InlayHintsOptions& options = {}, - PositionEncoding encoding = PositionEncoding::UTF16) - -> std::vector; - auto signature_help(CompilationParams& params, const SignatureHelpOptions& options = {}) -> protocol::SignatureHelp; diff --git a/src/feature/folding_ranges.cpp b/src/feature/folding_ranges.cpp index df4f98a0..6f53fb99 100644 --- a/src/feature/folding_ranges.cpp +++ b/src/feature/folding_ranges.cpp @@ -53,12 +53,6 @@ auto to_kind(FoldingKind kind) -> protocol::FoldingRangeKind { return protocol::FoldingRangeKind(protocol::FoldingRangeKind::region); } -struct RawFoldingRange { - LocalSourceRange range; - std::optional kind; - std::string collapsed_text; -}; - class FoldingRangeCollector : public FilteredASTVisitor { public: explicit FoldingRangeCollector(CompilationUnitRef unit) : FilteredASTVisitor(unit, true) {} @@ -185,7 +179,7 @@ public: return true; } - auto collect() -> std::vector { + auto collect() -> std::vector { TraverseDecl(unit.tu()); auto directives_it = unit.directives().find(unit.interested_file()); @@ -193,7 +187,7 @@ public: collect_directives(directives_it->second); } - std::ranges::sort(ranges, [](const RawFoldingRange& lhs, const RawFoldingRange& rhs) { + std::ranges::sort(ranges, [](const FoldingRange& lhs, const FoldingRange& rhs) { if(lhs.range.begin != rhs.range.begin) { return lhs.range.begin < rhs.range.begin; } @@ -343,14 +337,18 @@ private: } private: - std::vector ranges; + std::vector ranges; }; } // namespace +auto folding_ranges(CompilationUnitRef unit) -> std::vector { + return FoldingRangeCollector(unit).collect(); +} + auto folding_ranges(CompilationUnitRef unit, PositionEncoding encoding) -> std::vector { - auto collected = FoldingRangeCollector(unit).collect(); + auto collected = folding_ranges(unit); PositionMapper converter(unit.interested_content(), encoding); std::vector result; diff --git a/src/feature/inlay_hints.cpp b/src/feature/inlay_hints.cpp index a0eebd50..2e21901b 100644 --- a/src/feature/inlay_hints.cpp +++ b/src/feature/inlay_hints.cpp @@ -26,22 +26,6 @@ using llvm::dyn_cast_or_null; // For now, inlay hints are always anchored at the left or right of their range. enum class HintSide { Left, Right }; -enum class HintCategory : std::uint8_t { - Parameter, - DefaultArgument, - Type, - Designator, - BlockEnd, -}; - -struct RawInlayHint { - std::uint32_t offset = 0; - HintCategory kind = HintCategory::Type; - std::string label; - bool padding_left = false; - bool padding_right = false; -}; - bool is_expanded_from_param_pack(const clang::ParmVarDecl* param) { return ast::underlying_pack_type(param) != nullptr; } @@ -123,7 +107,7 @@ struct Callee { class Builder { public: - Builder(std::vector& result, + Builder(std::vector& result, CompilationUnitRef unit, LocalSourceRange restrict_range, const InlayHintsOptions& options) : @@ -499,7 +483,7 @@ public: bool pad_left = prefix.consume_front(" "); bool pad_right = suffix.consume_back(" "); - RawInlayHint hint{ + InlayHint hint{ .offset = offset, .kind = kind, .label = (prefix + label + suffix).str(), @@ -554,7 +538,7 @@ public: } private: - std::vector& result; + std::vector& result; CompilationUnitRef unit; LocalSourceRange restrict_range; const InlayHintsOptions& options; @@ -913,36 +897,43 @@ private: } // namespace -auto inlay_hints(CompilationUnitRef unit, - LocalSourceRange target, - const InlayHintsOptions& options, - PositionEncoding encoding) -> std::vector { +auto inlay_hints(CompilationUnitRef unit, LocalSourceRange target, const InlayHintsOptions& options) + -> std::vector { if(!options.enabled) { return {}; } - std::vector raw_hints; + std::vector raw_hints; Builder builder(raw_hints, unit, target, options); Visitor visitor(builder, unit, target, options); visitor.TraverseDecl(unit.tu()); - std::ranges::sort(raw_hints, [](const RawInlayHint& lhs, const RawInlayHint& rhs) { + std::ranges::sort(raw_hints, [](const InlayHint& lhs, const InlayHint& rhs) { return std::tie(lhs.offset, lhs.label, lhs.kind, lhs.padding_left, lhs.padding_right) < std::tie(rhs.offset, rhs.label, rhs.kind, rhs.padding_left, rhs.padding_right); }); auto unique_begin = - std::ranges::unique(raw_hints, [](const RawInlayHint& lhs, const RawInlayHint& rhs) { + std::ranges::unique(raw_hints, [](const InlayHint& lhs, const InlayHint& rhs) { return lhs.offset == rhs.offset && lhs.kind == rhs.kind && lhs.label == rhs.label && lhs.padding_left == rhs.padding_left && lhs.padding_right == rhs.padding_right; }); raw_hints.erase(unique_begin.begin(), unique_begin.end()); + return raw_hints; +} + +auto inlay_hints(CompilationUnitRef unit, + LocalSourceRange target, + const InlayHintsOptions& options, + PositionEncoding encoding) -> std::vector { + auto collected = inlay_hints(unit, target, options); + PositionMapper converter(unit.interested_content(), encoding); std::vector hints; - hints.reserve(raw_hints.size()); + hints.reserve(collected.size()); - for(const auto& hint: raw_hints) { + for(const auto& hint: collected) { protocol::InlayHint out{ .position = *converter.to_position(hint.offset), .label = hint.label, diff --git a/src/feature/semantic_tokens.cpp b/src/feature/semantic_tokens.cpp index 4f81ae82..13cf0674 100644 --- a/src/feature/semantic_tokens.cpp +++ b/src/feature/semantic_tokens.cpp @@ -18,12 +18,6 @@ namespace clice::feature { namespace { -struct RawToken { - LocalSourceRange range; - SymbolKind kind = SymbolKind::Invalid; - std::uint32_t modifiers = 0; -}; - void add_modifier(std::uint32_t& modifiers, SymbolModifiers::Kind kind) { modifiers |= SymbolModifiers::to_mask(kind); } @@ -166,7 +160,7 @@ class SemanticTokensCollector : public SemanticVisitor public: explicit SemanticTokensCollector(CompilationUnitRef unit) : SemanticVisitor(unit, true) {} - auto collect() -> std::vector { + auto collect() -> std::vector { highlight_lexical(unit.interested_file()); run(); highlight_modules(); @@ -398,7 +392,7 @@ private: } } - static void resolve_conflict(RawToken& last, const RawToken& current) { + static void resolve_conflict(SemanticToken& last, const SemanticToken& current) { if(last.kind == SymbolKind::Conflict) { return; } @@ -414,14 +408,14 @@ private: } void merge_tokens() { - std::ranges::sort(tokens, [](const RawToken& lhs, const RawToken& rhs) { + std::ranges::sort(tokens, [](const SemanticToken& lhs, const SemanticToken& rhs) { if(lhs.range.begin != rhs.range.begin) { return lhs.range.begin < rhs.range.begin; } return lhs.range.end < rhs.range.end; }); - std::vector merged; + std::vector merged; merged.reserve(tokens.size()); for(const auto& token: tokens) { @@ -448,7 +442,7 @@ private: } public: - std::vector tokens; + std::vector tokens; }; class SemanticTokenEncoder { @@ -458,7 +452,7 @@ public: protocol::SemanticTokens& output) : content(content), converter(content, encoding), output(output) {} - void append(const RawToken& token) { + void append(const SemanticToken& token) { if(!token.range.valid() || token.range.end <= token.range.begin || token.range.end > content.size()) { return; @@ -542,10 +536,14 @@ private: } // namespace +auto semantic_tokens(CompilationUnitRef unit) -> std::vector { + SemanticTokensCollector collector(unit); + return collector.collect(); +} + auto semantic_tokens(CompilationUnitRef unit, PositionEncoding encoding) -> protocol::SemanticTokens { - SemanticTokensCollector collector(unit); - auto tokens = collector.collect(); + auto tokens = semantic_tokens(unit); protocol::SemanticTokens result; result.data.reserve(tokens.size() * 5); diff --git a/src/server/worker/stateful_worker.cpp b/src/server/worker/stateful_worker.cpp index 3cda3289..18999ed4 100644 --- a/src/server/worker/stateful_worker.cpp +++ b/src/server/worker/stateful_worker.cpp @@ -245,26 +245,33 @@ void StatefulWorker::register_handlers() { co_return kota::codec::RawValue{"[]"}; case K::SemanticTokens: co_return co_await with_ast(params.path, [&](DocumentEntry& doc) { - return to_raw(feature::semantic_tokens(doc.unit)); + return to_raw( + feature::semantic_tokens(doc.unit, feature::PositionEncoding::UTF16)); }); case K::InlayHints: co_return co_await with_ast(params.path, [&](DocumentEntry& doc) { auto range = params.range; if(range.begin == static_cast(-1)) range = LocalSourceRange{0, static_cast(doc.text.size())}; - return to_raw(feature::inlay_hints(doc.unit, range)); + return to_raw(feature::inlay_hints(doc.unit, + range, + {}, + feature::PositionEncoding::UTF16)); }); case K::FoldingRange: co_return co_await with_ast(params.path, [&](DocumentEntry& doc) { - return to_raw(feature::folding_ranges(doc.unit)); + return to_raw( + feature::folding_ranges(doc.unit, feature::PositionEncoding::UTF16)); }); case K::DocumentSymbol: co_return co_await with_ast(params.path, [&](DocumentEntry& doc) { - return to_raw(feature::document_symbols(doc.unit)); + return to_raw( + feature::document_symbols(doc.unit, feature::PositionEncoding::UTF16)); }); case K::DocumentLink: co_return co_await with_ast(params.path, [&](DocumentEntry& doc) { - return to_raw(feature::document_links(doc.unit)); + return to_raw( + feature::document_links(doc.unit, feature::PositionEncoding::UTF16)); }); case K::CodeAction: // TODO: Implement code actions diff --git a/src/server/workspace/config.cpp b/src/server/workspace/config.cpp index ba44088d..0bd567c4 100644 --- a/src/server/workspace/config.cpp +++ b/src/server/workspace/config.cpp @@ -156,13 +156,13 @@ std::optional Config::load(llvm::StringRef path, llvm::StringRef workspa } std::optional Config::load_from_json(llvm::StringRef json, llvm::StringRef workspace_root) { - auto result = kota::codec::json::from_json(json); + Config config{}; + auto result = kota::codec::json::from_json(json, config); if(!result) { - LOG_WARN("Failed to parse initializationOptions JSON: {}", result.error().message()); + LOG_WARN("Failed to parse initializationOptions JSON: {}", result.error().message); return std::nullopt; } - auto config = std::move(*result); config.apply_defaults(workspace_root); LOG_INFO("Loaded config from initializationOptions"); return config; diff --git a/tests/corpus/statements/if/basic_if.cpp b/tests/corpus/statements/if/basic_if.cpp new file mode 100644 index 00000000..754d2ea8 --- /dev/null +++ b/tests/corpus/statements/if/basic_if.cpp @@ -0,0 +1,36 @@ +// basic if and if-else +namespace basic_if { + +int abs_val(int x) { + if(x < 0) + return -x; + return x; +} + +const char* sign(int x) { + if(x > 0) { + return "positive"; + } else if(x < 0) { + return "negative"; + } else { + return "zero"; + } +} + +// dangling else: else binds to nearest if +int nested_if(int a, int b) { + if(a > 0) + if(b > 0) + return 1; + else + return 2; + return 0; +} + +void test() { + [[maybe_unused]] int r1 = abs_val(-3); + [[maybe_unused]] auto r2 = sign(5); + [[maybe_unused]] int r3 = nested_if(1, -1); +} + +} // namespace basic_if diff --git a/tests/snapshots/document_symbol/snapshot/statements/if/basic_if.cpp.snap.yml b/tests/snapshots/document_symbol/snapshot/statements/if/basic_if.cpp.snap.yml new file mode 100644 index 00000000..ef5b3748 --- /dev/null +++ b/tests/snapshots/document_symbol/snapshot/statements/if/basic_if.cpp.snap.yml @@ -0,0 +1,14 @@ +--- +source: document_symbol_tests.cpp +created_at: 2026-05-20 +input_file: statements/if/basic_if.cpp +--- +- { name: "basic_if", kind: Namespace, range: "1:0-35:1", selection_range: "1:10-1:18" } +- { name: "abs_val", kind: Function, range: "3:0-7:1", selection_range: "3:4-3:11", detail: "int (int)" } +- { name: "sign", kind: Function, range: "9:0-17:1", selection_range: "9:12-9:16", detail: "const char *(int)" } +- { name: "nested_if", kind: Function, range: "20:0-27:1", selection_range: "20:4-20:13", detail: "int (int, int)" } +- { name: "test", kind: Function, range: "29:0-33:1", selection_range: "29:5-29:9", detail: "void ()" } +- { name: "r1", kind: Variable, range: "30:21-30:41", selection_range: "30:25-30:27", detail: "int" } +- { name: "r2", kind: Variable, range: "31:21-31:38", selection_range: "31:26-31:28", detail: "const char *" } +- { name: "r3", kind: Variable, range: "32:21-32:46", selection_range: "32:25-32:27", detail: "int" } + diff --git a/tests/snapshots/folding_range/snapshot/statements/if/basic_if.cpp.snap.yml b/tests/snapshots/folding_range/snapshot/statements/if/basic_if.cpp.snap.yml new file mode 100644 index 00000000..f3216f7b --- /dev/null +++ b/tests/snapshots/folding_range/snapshot/statements/if/basic_if.cpp.snap.yml @@ -0,0 +1,11 @@ +--- +source: folding_range_tests.cpp +created_at: 2026-05-20 +input_file: statements/if/basic_if.cpp +--- +- { range: "1:19-35:1", kind: namespace, collapsed_text: "{...}" } +- { range: "3:19-7:1", kind: functionBody, collapsed_text: "{...}" } +- { range: "9:24-17:1", kind: functionBody, collapsed_text: "{...}" } +- { range: "20:28-27:1", kind: functionBody, collapsed_text: "{...}" } +- { range: "29:12-33:1", kind: functionBody, collapsed_text: "{...}" } + diff --git a/tests/snapshots/inlay_hint/snapshot/statements/if/basic_if.cpp.snap.yml b/tests/snapshots/inlay_hint/snapshot/statements/if/basic_if.cpp.snap.yml new file mode 100644 index 00000000..072d60c7 --- /dev/null +++ b/tests/snapshots/inlay_hint/snapshot/statements/if/basic_if.cpp.snap.yml @@ -0,0 +1,11 @@ +--- +source: inlay_hint_tests.cpp +created_at: 2026-05-20 +input_file: statements/if/basic_if.cpp +--- +- { pos: "30:38", kind: Parameter, label: "x:", padding_right: true } +- { pos: "31:28", kind: Type, label: ": const char *" } +- { pos: "31:36", kind: Parameter, label: "x:", padding_right: true } +- { pos: "32:40", kind: Parameter, label: "a:", padding_right: true } +- { pos: "32:43", kind: Parameter, label: "b:", padding_right: true } + diff --git a/tests/snapshots/semantic_tokens/snapshot/statements/if/basic_if.cpp.snap.yml b/tests/snapshots/semantic_tokens/snapshot/statements/if/basic_if.cpp.snap.yml new file mode 100644 index 00000000..bf9d146d --- /dev/null +++ b/tests/snapshots/semantic_tokens/snapshot/statements/if/basic_if.cpp.snap.yml @@ -0,0 +1,75 @@ +--- +source: semantic_tokens_tests.cpp +created_at: 2026-05-20 +input_file: statements/if/basic_if.cpp +--- +- { loc: "0:0", text: "// basic if and if-else", kind: Comment } +- { loc: "1:0", text: "namespace", kind: Keyword } +- { loc: "1:10", text: "basic_if", kind: Namespace, modifiers: [Definition] } +- { loc: "3:0", text: "int", kind: Keyword } +- { loc: "3:4", text: "abs_val", kind: Function, modifiers: [Definition] } +- { loc: "3:12", text: "int", kind: Keyword } +- { loc: "3:16", text: "x", kind: Parameter, modifiers: [Definition] } +- { loc: "4:4", text: "if", kind: Keyword } +- { loc: "4:7", text: "x", kind: Parameter } +- { loc: "4:11", text: "0", kind: Number } +- { loc: "5:8", text: "return", kind: Keyword } +- { loc: "5:16", text: "x", kind: Parameter } +- { loc: "6:4", text: "return", kind: Keyword } +- { loc: "6:11", text: "x", kind: Parameter } +- { loc: "9:0", text: "const", kind: Keyword } +- { loc: "9:6", text: "char", kind: Keyword } +- { loc: "9:12", text: "sign", kind: Function, modifiers: [Definition, Readonly] } +- { loc: "9:17", text: "int", kind: Keyword } +- { loc: "9:21", text: "x", kind: Parameter, modifiers: [Definition] } +- { loc: "10:4", text: "if", kind: Keyword } +- { loc: "10:7", text: "x", kind: Parameter } +- { loc: "10:11", text: "0", kind: Number } +- { loc: "11:8", text: "return", kind: Keyword } +- { loc: "11:15", text: "\"positive\"", kind: String } +- { loc: "12:6", text: "else", kind: Keyword } +- { loc: "12:11", text: "if", kind: Keyword } +- { loc: "12:14", text: "x", kind: Parameter } +- { loc: "12:18", text: "0", kind: Number } +- { loc: "13:8", text: "return", kind: Keyword } +- { loc: "13:15", text: "\"negative\"", kind: String } +- { loc: "14:6", text: "else", kind: Keyword } +- { loc: "15:8", text: "return", kind: Keyword } +- { loc: "15:15", text: "\"zero\"", kind: String } +- { loc: "19:0", text: "// dangling else: else binds to nearest if", kind: Comment } +- { loc: "20:0", text: "int", kind: Keyword } +- { loc: "20:4", text: "nested_if", kind: Function, modifiers: [Definition] } +- { loc: "20:14", text: "int", kind: Keyword } +- { loc: "20:18", text: "a", kind: Parameter, modifiers: [Definition] } +- { loc: "20:21", text: "int", kind: Keyword } +- { loc: "20:25", text: "b", kind: Parameter, modifiers: [Definition] } +- { loc: "21:4", text: "if", kind: Keyword } +- { loc: "21:7", text: "a", kind: Parameter } +- { loc: "21:11", text: "0", kind: Number } +- { loc: "22:8", text: "if", kind: Keyword } +- { loc: "22:11", text: "b", kind: Parameter } +- { loc: "22:15", text: "0", kind: Number } +- { loc: "23:12", text: "return", kind: Keyword } +- { loc: "23:19", text: "1", kind: Number } +- { loc: "24:8", text: "else", kind: Keyword } +- { loc: "25:12", text: "return", kind: Keyword } +- { loc: "25:19", text: "2", kind: Number } +- { loc: "26:4", text: "return", kind: Keyword } +- { loc: "26:11", text: "0", kind: Number } +- { loc: "29:0", text: "void", kind: Keyword } +- { loc: "29:5", text: "test", kind: Function, modifiers: [Definition] } +- { loc: "30:21", text: "int", kind: Keyword } +- { loc: "30:25", text: "r1", kind: Variable, modifiers: [Definition] } +- { loc: "30:30", text: "abs_val", kind: Function } +- { loc: "30:39", text: "3", kind: Number } +- { loc: "31:21", text: "auto", kind: Keyword } +- { loc: "31:26", text: "r2", kind: Variable, modifiers: [Definition, Readonly] } +- { loc: "31:31", text: "sign", kind: Function, modifiers: [Readonly] } +- { loc: "31:36", text: "5", kind: Number } +- { loc: "32:21", text: "int", kind: Keyword } +- { loc: "32:25", text: "r3", kind: Variable, modifiers: [Definition] } +- { loc: "32:30", text: "nested_if", kind: Function } +- { loc: "32:40", text: "1", kind: Number } +- { loc: "32:44", text: "1", kind: Number } +- { loc: "35:3", text: "// namespace basic_if", kind: Comment } + diff --git a/tests/snapshots/tu_index/snapshot/statements/if/basic_if.cpp.snap.yml b/tests/snapshots/tu_index/snapshot/statements/if/basic_if.cpp.snap.yml new file mode 100644 index 00000000..0ffe2120 --- /dev/null +++ b/tests/snapshots/tu_index/snapshot/statements/if/basic_if.cpp.snap.yml @@ -0,0 +1,28 @@ +--- +source: tu_index_tests.cpp +created_at: 2026-05-20 +input_file: statements/if/basic_if.cpp +--- +- { loc: "1:10", kind: Namespace, text: "basic_if", relations: [Definition] } +- { loc: "3:4", kind: Function, text: "abs_val", relations: [Definition] } +- { loc: "3:16", kind: Parameter, text: "x", relations: [Definition] } +- { loc: "4:7", kind: Parameter, text: "x", relations: [Reference] } +- { loc: "5:16", kind: Parameter, text: "x", relations: [Reference] } +- { loc: "6:11", kind: Parameter, text: "x", relations: [Reference] } +- { loc: "9:12", kind: Function, text: "sign", relations: [Definition] } +- { loc: "9:21", kind: Parameter, text: "x", relations: [Definition] } +- { loc: "10:7", kind: Parameter, text: "x", relations: [Reference] } +- { loc: "12:14", kind: Parameter, text: "x", relations: [Reference] } +- { loc: "20:4", kind: Function, text: "nested_if", relations: [Definition] } +- { loc: "20:18", kind: Parameter, text: "a", relations: [Definition] } +- { loc: "20:25", kind: Parameter, text: "b", relations: [Definition] } +- { loc: "21:7", kind: Parameter, text: "a", relations: [Reference] } +- { loc: "22:11", kind: Parameter, text: "b", relations: [Reference] } +- { loc: "29:5", kind: Function, text: "test", relations: [Definition] } +- { loc: "30:25", kind: Variable, text: "r1", relations: [Definition] } +- { loc: "30:30", kind: Function, text: "abs_val", relations: [Reference] } +- { loc: "31:26", kind: Variable, text: "r2", relations: [Definition] } +- { loc: "31:31", kind: Function, text: "sign", relations: [Reference] } +- { loc: "32:25", kind: Variable, text: "r3", relations: [Definition] } +- { loc: "32:30", kind: Function, text: "nested_if", relations: [Reference] } + diff --git a/tests/unit/feature/document_link_tests.cpp b/tests/unit/feature/document_link_tests.cpp index 4674fecd..9da70053 100644 --- a/tests/unit/feature/document_link_tests.cpp +++ b/tests/unit/feature/document_link_tests.cpp @@ -11,7 +11,7 @@ namespace { namespace protocol = kota::ipc::protocol; -TEST_SUITE(DocumentLink, Tester) { +TEST_SUITE(document_link, Tester) { std::vector links; @@ -136,7 +136,7 @@ ABCDE EXPECT_LINK(0, "0", TestVFS::path("data.bin")); } -}; // TEST_SUITE(DocumentLink) +}; // TEST_SUITE(document_link) } // namespace diff --git a/tests/unit/feature/document_symbol_tests.cpp b/tests/unit/feature/document_symbol_tests.cpp index 792beaaa..603edc1f 100644 --- a/tests/unit/feature/document_symbol_tests.cpp +++ b/tests/unit/feature/document_symbol_tests.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -7,13 +8,15 @@ #include "test/tester.h" #include "feature/feature.h" +#include "kota/meta/enum.h" + namespace clice::testing { namespace { namespace protocol = kota::ipc::protocol; -TEST_SUITE(DocumentSymbol, Tester) { +TEST_SUITE(document_symbol, Tester) { std::vector symbols; @@ -180,7 +183,57 @@ VAR(test) ASSERT_EQ(total_size(symbols), 3U); } -}; // TEST_SUITE(DocumentSymbol) +void format_document_symbols(std::string& out, + const feature::PositionMapper& mapper, + llvm::ArrayRef nodes, + int depth) { + auto pad = std::string(depth * 2, ' '); + for(auto& node: nodes) { + auto kind = kota::meta::enum_name(static_cast(node.kind), "Unknown"); + auto start = mapper.to_position(node.range.begin); + auto end = mapper.to_position(node.range.end); + if(!start || !end) + continue; + auto sel_start = mapper.to_position(node.selection_range.begin); + auto sel_end = mapper.to_position(node.selection_range.end); + out += std::format("- {}{{ name: {}, kind: {}, range: \"{}:{}-{}:{}\"", + pad, + yaml_str(node.name), + kind, + start->line, + start->character, + end->line, + end->character); + if(sel_start && sel_end) { + out += std::format(", selection_range: \"{}:{}-{}:{}\"", + sel_start->line, + sel_start->character, + sel_end->line, + sel_end->character); + } + if(!node.detail.empty()) { + out += std::format(", detail: {}", yaml_str(node.detail)); + } + out += " }\n"; + if(!node.children.empty()) { + format_document_symbols(out, mapper, node.children, depth + 1); + } + } +} + +TEST_CASE(snapshot) { + ASSERT_SNAPSHOT_GLOB(corpus_dir, "**/*.cpp", [&](std::string_view path) -> std::string { + if(!compile_file(path)) + return "COMPILE_ERROR"; + auto content = unit->interested_content(); + feature::PositionMapper mapper(content, feature::PositionEncoding::UTF8); + std::string result; + format_document_symbols(result, mapper, feature::document_symbols(*unit), 0); + return result; + }); +} + +}; // TEST_SUITE(document_symbol) } // namespace diff --git a/tests/unit/feature/folding_range_tests.cpp b/tests/unit/feature/folding_range_tests.cpp index aec7c450..b7a75a55 100644 --- a/tests/unit/feature/folding_range_tests.cpp +++ b/tests/unit/feature/folding_range_tests.cpp @@ -11,7 +11,7 @@ namespace { namespace protocol = kota::ipc::protocol; -TEST_SUITE(FoldingRange, Tester) { +TEST_SUITE(folding_range, Tester) { std::vector ranges; @@ -429,7 +429,36 @@ $(1)#pragma region level1 )cpp"); } -}; // TEST_SUITE(FoldingRange) +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(*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 diff --git a/tests/unit/feature/inlay_hint_tests.cpp b/tests/unit/feature/inlay_hint_tests.cpp index 00cbd0f2..f6eb89df 100644 --- a/tests/unit/feature/inlay_hint_tests.cpp +++ b/tests/unit/feature/inlay_hint_tests.cpp @@ -1,16 +1,19 @@ +#include #include #include "test/test.h" #include "test/tester.h" #include "feature/feature.h" +#include "kota/meta/enum.h" + namespace clice::testing { namespace { namespace protocol = kota::ipc::protocol; -TEST_SUITE(InlayHint, Tester) { +TEST_SUITE(inlay_hint, Tester) { std::vector hints; llvm::DenseMap hints_map; @@ -1529,6 +1532,38 @@ TEST_CASE(Dependent, skip = true) { EXPECT_HINT("2", "par3:"); } -}; // TEST_SUITE(InlayHint) +TEST_CASE(snapshot) { + ASSERT_SNAPSHOT_GLOB(corpus_dir, "**/*.cpp", [&](std::string_view path) -> std::string { + if(!compile_file(path)) + return "COMPILE_ERROR"; + auto content = unit->interested_content(); + LocalSourceRange range(0, content.size()); + auto hints = feature::inlay_hints(*unit, range); + feature::PositionMapper mapper(content, feature::PositionEncoding::UTF8); + std::string result; + for(auto& hint: hints) { + auto pos = mapper.to_position(hint.offset); + if(!pos) + continue; + auto kind = kota::meta::enum_name(hint.kind, "Unknown"); + result += std::format("- {{ pos: \"{}:{}\", kind: {}, label: {}", + pos->line, + pos->character, + kind, + yaml_str(hint.label)); + if(hint.padding_left) { + result += ", padding_left: true"; + } + if(hint.padding_right) { + result += ", padding_right: true"; + } + result += " }\n"; + } + return result; + }); +} + +}; // TEST_SUITE(inlay_hint) + } // namespace } // namespace clice::testing diff --git a/tests/unit/feature/semantic_tokens_tests.cpp b/tests/unit/feature/semantic_tokens_tests.cpp index e83aac89..71e922b1 100644 --- a/tests/unit/feature/semantic_tokens_tests.cpp +++ b/tests/unit/feature/semantic_tokens_tests.cpp @@ -9,6 +9,8 @@ #include "feature/feature.h" #include "semantic/symbol_kind.h" +#include "kota/meta/enum.h" + namespace clice::testing { namespace { @@ -99,7 +101,7 @@ auto decode_relative_tokens(const protocol::SemanticTokens& tokens) -> std::vect return result; } -TEST_SUITE(SemanticTokens, Tester) { +TEST_SUITE(semantic_tokens, Tester) { protocol::SemanticTokens tokens; std::vector decoded; @@ -539,7 +541,53 @@ void f() { EXPECT_TOKEN("v2", SymbolKind::Variable, definition); } -}; // TEST_SUITE(SemanticTokens) +TEST_CASE(snapshot) { + ASSERT_SNAPSHOT_GLOB(corpus_dir, "**/*.cpp", [&](std::string_view path) -> std::string { + if(!compile_file(path)) + return "COMPILE_ERROR"; + auto content = unit->interested_content(); + auto tokens = feature::semantic_tokens(*unit); + feature::PositionMapper mapper(content, feature::PositionEncoding::UTF8); + std::string result; + for(auto& token: tokens) { + if(!token.range.valid() || token.range.end <= token.range.begin || + token.range.end > content.size()) + continue; + + auto pos = mapper.to_position(token.range.begin); + if(!pos) + continue; + + auto text = content.substr(token.range.begin, token.range.length()); + auto kind = kota::meta::enum_name(static_cast(token.kind), "Unknown"); + + result += std::format("- {{ loc: \"{}:{}\", text: {}, kind: {}", + pos->line, + pos->character, + yaml_str(text), + kind); + + std::string mods; + for(std::uint32_t i = 0; i < 32; ++i) { + if(token.modifiers & (1u << i)) { + auto name = kota::meta::enum_name(static_cast(i)); + if(!name.empty()) { + if(!mods.empty()) + mods += ", "; + mods += name; + } + } + } + if(!mods.empty()) { + result += std::format(", modifiers: [{}]", mods); + } + result += " }\n"; + } + return result; + }); +} + +}; // TEST_SUITE(semantic_tokens) } // namespace diff --git a/tests/unit/index/tu_index_tests.cpp b/tests/unit/index/tu_index_tests.cpp index 8deadbc2..01b624d4 100644 --- a/tests/unit/index/tu_index_tests.cpp +++ b/tests/unit/index/tu_index_tests.cpp @@ -1,13 +1,18 @@ +#include +#include #include #include "test/test.h" #include "test/tester.h" +#include "feature/feature.h" #include "index/tu_index.h" +#include "kota/meta/enum.h" + namespace clice::testing { namespace { -TEST_SUITE(TUIndex, Tester) { +TEST_SUITE(tu_index, Tester) { index::TUIndex tu_index; @@ -500,6 +505,64 @@ TEST_CASE(SymbolKinds) { check_kind("ns", SymbolKind::Namespace); } -}; // TEST_SUITE(TUIndex) +TEST_CASE(snapshot) { + ASSERT_SNAPSHOT_GLOB(corpus_dir, "**/*.cpp", [&](std::string_view path) -> std::string { + if(!compile_file(path)) + return "COMPILE_ERROR"; + auto idx = index::TUIndex::build(*unit); + auto content = unit->interested_content(); + feature::PositionMapper mapper(content, feature::PositionEncoding::UTF8); + std::string result; + + auto sorted = idx.main_file_index.occurrences; + std::ranges::sort(sorted, [](auto& lhs, auto& rhs) { + return std::tuple(lhs.range.begin, lhs.range.end, lhs.target) < + std::tuple(rhs.range.begin, rhs.range.end, rhs.target); + }); + + for(auto& occ: sorted) { + auto text = content.substr(occ.range.begin, occ.range.end - occ.range.begin); + auto pos = mapper.to_position(occ.range.begin); + if(!pos) + continue; + + auto sym_it = idx.symbols.find(occ.target); + std::string_view kind_name = "?"; + if(sym_it != idx.symbols.end()) { + kind_name = + kota::meta::enum_name(static_cast(sym_it->second.kind), + "Unknown"); + } + + result += std::format("- {{ loc: \"{}:{}\", kind: {}, text: {}", + pos->line, + pos->character, + kind_name, + yaml_str(text)); + + auto rel_it = idx.main_file_index.relations.find(occ.target); + if(rel_it != idx.main_file_index.relations.end()) { + std::string rels; + for(auto& rel: rel_it->second) { + if(rel.range != occ.range) + continue; + if(!rels.empty()) + rels += ", "; + rels += kota::meta::enum_name(static_cast(rel.kind), "?"); + } + if(!rels.empty()) { + result += std::format(", relations: [{}]", rels); + } + } + + result += " }\n"; + } + + return result; + }); +} + +}; // TEST_SUITE(tu_index) + } // namespace } // namespace clice::testing diff --git a/tests/unit/test/platform.h b/tests/unit/test/platform.h index b58dbea8..d260fd89 100644 --- a/tests/unit/test/platform.h +++ b/tests/unit/test/platform.h @@ -1,3 +1,5 @@ +#include + #include "llvm/ADT/SmallString.h" #include "llvm/Support/MemoryBuffer.h" #include "llvm/Support/Path.h" @@ -5,6 +7,12 @@ namespace clice::testing { +/// Set by --test-dir from the command line; empty if not specified. +inline std::string test_dir; + +/// Set by --corpus-dir from the command line; empty if not specified. +inline std::string corpus_dir; + #ifdef _WIN32 constexpr inline bool Windows = true; #else diff --git a/tests/unit/test/tester.cpp b/tests/unit/test/tester.cpp index e5b96042..025e9b4b 100644 --- a/tests/unit/test/tester.cpp +++ b/tests/unit/test/tester.cpp @@ -230,6 +230,17 @@ bool Tester::compile_with_modules(llvm::StringRef standard) { 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; diff --git a/tests/unit/test/tester.h b/tests/unit/test/tester.h index c372b19e..b2b83f81 100644 --- a/tests/unit/test/tester.h +++ b/tests/unit/test/tester.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -63,6 +64,9 @@ struct Tester { bool compile_with_modules(llvm::StringRef standard = "-std=c++20"); + /// Read a file from disk and compile it directly (no VFS content needed). + bool compile_file(llvm::StringRef path, llvm::StringRef standard = "-std=c++20"); + /// Driver path: uses CompilationDatabase + toolchain cache, has system headers. void prepare_driver(llvm::StringRef standard = "-std=c++20"); @@ -85,4 +89,28 @@ struct Tester { void clear(); }; +inline std::string yaml_str(llvm::StringRef s) { + std::string result; + result.reserve(s.size() + 2); + result += '"'; + for(char c: s) { + switch(c) { + case '"': result += "\\\""; break; + case '\\': result += "\\\\"; break; + case '\n': result += "\\n"; break; + case '\r': result += "\\r"; break; + case '\t': result += "\\t"; break; + default: + if(static_cast(c) < 0x20) { + result += std::format("\\x{:02x}", static_cast(c)); + } else { + result += c; + } + break; + } + } + result += '"'; + return result; +} + } // namespace clice::testing diff --git a/tests/unit/unit_tests.cc b/tests/unit/unit_tests.cc index d00db022..9fc4d34c 100644 --- a/tests/unit/unit_tests.cc +++ b/tests/unit/unit_tests.cc @@ -1,6 +1,7 @@ #include #include +#include "test/platform.h" #include "support/logging.h" #include "kota/deco/deco.h" @@ -11,23 +12,20 @@ namespace { using kota::deco::decl::KVStyle; struct TestOptions { - DecoKV(style = KVStyle::JoinedOrSeparate, - names = {"--test-filter", "--test-filter="}, - help = "Filter tests by name", - required = false) - test_filter; + kota::zest::Options zest; - DecoKV(style = KVStyle::JoinedOrSeparate, - names = {"--log-level", "--log-level="}, - help = "Log level: trace/debug/info/warn/err", - required = false) + DecoKVStyled(KVStyle::JoinedOrSeparate, help = "log level: trace/debug/info/warn/err"; + required = false) log_level; - DecoKV(style = KVStyle::JoinedOrSeparate, - names = {"--test-dir", "--test-dir="}, - help = "Test data directory", - required = false) + DecoKVStyled(KVStyle::JoinedOrSeparate, meta_var = ""; help = "test data directory"; + required = false) test_dir; + + DecoKVStyled(KVStyle::JoinedOrSeparate, meta_var = ""; + help = "corpus directory for snapshot glob tests"; + required = false) + corpus_dir; }; } // namespace @@ -36,13 +34,20 @@ int main(int argc, const char** argv) { auto args = kota::deco::util::argvify(argc, argv); auto parsed = kota::deco::cli::parse(args); - std::string_view filter = {}; - if(parsed.has_value() && parsed->options.test_filter.has_value()) { - filter = *parsed->options.test_filter; + if(!parsed.has_value()) { + return 1; } - if(parsed.has_value() && parsed->options.log_level.has_value()) { - auto level = *parsed->options.log_level; + auto& opts = parsed->options; + + if(opts.test_dir.has_value()) + clice::testing::test_dir = *opts.test_dir; + + if(opts.corpus_dir.has_value()) + clice::testing::corpus_dir = *opts.corpus_dir; + + if(opts.log_level.has_value()) { + auto level = *opts.log_level; if(level == "trace") { clice::logging::options.level = clice::logging::Level::trace; } else if(level == "debug") { @@ -58,5 +63,5 @@ int main(int argc, const char** argv) { clice::logging::stderr_logger("test", clice::logging::options); - return kota::zest::Runner::instance().run_tests(filter); + return kota::zest::run_tests(std::move(opts.zest)); }