From bc048452931a814342a7c8446aed79afa51827d6 Mon Sep 17 00:00:00 2001 From: ykiko Date: Tue, 31 Mar 2026 16:57:48 +0800 Subject: [PATCH] refactor(tests): CMake-based CDB, workspace fixture, test cleanup (#378) ## Summary - **CMake-based CDB generation for module tests**: Replace hand-written compile_commands.json with CMakeLists.txt (CMake 3.28 `FILE_SET CXX_MODULES`) in all 26 `tests/data/modules/*/` directories. CDB is generated on-the-fly via `cmake -G Ninja` during test setup. - **`@pytest.mark.workspace()` decorator**: Introduce a marker + fixture pattern so tests declare their workspace via decorator and receive a resolved `workspace` path. The fixture auto-generates CDB when a CMakeLists.txt is present. - **`CliceClient` helper methods**: Add `initialize()`, `open()`, `wait_diagnostics()`, and `open_and_wait()` to reduce boilerplate across all test files. - **Use `asyncio_mode = "auto"`**: Switch from `@pytest_asyncio.fixture` + `@pytest.mark.asyncio` to `@pytest.fixture` + auto mode for proper Pylance type inference on fixtures. - **Test cleanup**: Remove redundant section separators and docstrings, delete `tests/pyproject.toml` (config moved to `pytest.ini`). - **Format task**: Add `.cppm` to `format-cpp` glob pattern. - **CI fix**: Disable `CMAKE_CXX_SCAN_FOR_MODULES` and prefer pixi clang++ to fix macOS CI where CMake rejects module scanning. ## Test plan - [x] All 26 module test directories have CMakeLists.txt with FILE_SET CXX_MODULES - [x] generate_cdb() produces valid compile_commands.json with module flags - [x] Integration tests pass locally - [ ] CI passes on all platforms (Linux, macOS, Windows) ## Summary by CodeRabbit * **Tests** * Unified fixtures and client workflow: new init/open/wait helpers, workspace marker support, bounded diagnostics waiting, CMake-based compilation-database generation, and directory-backed temp-file workflows; enabled asyncio test mode. * **Chores** * Added many C++20 module test projects and test data; removed prior test pyproject in favor of pytest config; updated formatter to include .cppm files. * **Style** * Reformatted many module/source implementations to consistent multi-line function bodies. --------- Co-authored-by: Claude Opus 4.6 --- CMakeLists.txt | 19 +- cmake/toolchain.cmake | 24 +- pixi.toml | 2 +- scripts/activate_asan.bat | 2 +- tests/conftest.py | 127 +++- .../modules/chained_modules/CMakeLists.txt | 13 + tests/data/modules/chained_modules/mod_a.cppm | 5 +- tests/data/modules/chained_modules/mod_b.cppm | 5 +- .../circular_module_dependency/CMakeLists.txt | 13 + .../circular_module_dependency/cycle_a.cppm | 5 +- .../circular_module_dependency/cycle_b.cppm | 5 +- .../circular_module_dependency/ok.cppm | 5 +- .../CMakeLists.txt | 13 + .../class_export_and_inheritance/circle.cppm | 7 +- .../class_export_and_inheritance/shape.cppm | 1 + .../consumer_imports_module/CMakeLists.txt | 14 + .../modules/consumer_imports_module/math.cppm | 5 +- tests/data/modules/deep_chain/CMakeLists.txt | 13 + tests/data/modules/deep_chain/m1.cppm | 5 +- tests/data/modules/deep_chain/m2.cppm | 5 +- tests/data/modules/deep_chain/m3.cppm | 5 +- tests/data/modules/deep_chain/m4.cppm | 5 +- tests/data/modules/deep_chain/m5.cppm | 5 +- .../modules/diamond_modules/CMakeLists.txt | 13 + tests/data/modules/diamond_modules/base.cppm | 5 +- tests/data/modules/diamond_modules/left.cppm | 5 +- tests/data/modules/diamond_modules/right.cppm | 5 +- tests/data/modules/diamond_modules/top.cppm | 5 +- .../modules/dotted_module_name/CMakeLists.txt | 13 + .../data/modules/dotted_module_name/app.cppm | 5 +- tests/data/modules/dotted_module_name/io.cppm | 1 + .../data/modules/export_block/CMakeLists.txt | 13 + tests/data/modules/export_block/block.cppm | 18 +- tests/data/modules/export_block/consumer.cppm | 5 +- .../modules/export_namespace/CMakeLists.txt | 13 + tests/data/modules/export_namespace/calc.cppm | 5 +- tests/data/modules/export_namespace/ns.cppm | 12 +- .../global_module_fragment/CMakeLists.txt | 14 + .../modules/global_module_fragment/gmf.cppm | 5 +- .../modules/gmf_with_import/CMakeLists.txt | 14 + tests/data/modules/gmf_with_import/base.cppm | 5 +- .../modules/gmf_with_import/combined.cppm | 5 +- .../hover_on_imported_symbol/CMakeLists.txt | 14 + .../hover_on_imported_symbol/defs.cppm | 5 +- .../independent_modules/CMakeLists.txt | 13 + tests/data/modules/independent_modules/x.cppm | 5 +- tests/data/modules/independent_modules/y.cppm | 5 +- .../module_compile_error/CMakeLists.txt | 13 + .../modules/module_compile_error/bad.cppm | 5 +- .../modules/module_compile_error/good.cppm | 5 +- .../module_implementation_unit/CMakeLists.txt | 13 + .../modules/module_partitions/CMakeLists.txt | 13 + tests/data/modules/module_partitions/lib.cppm | 5 +- .../modules/module_partitions/part_a.cppm | 5 +- .../modules/module_partitions/part_b.cppm | 5 +- .../no_modules_plain_cpp/CMakeLists.txt | 8 + .../modules/partition_chain/CMakeLists.txt | 13 + tests/data/modules/partition_chain/core.cppm | 5 +- tests/data/modules/partition_chain/types.cppm | 5 +- .../partition_interface/CMakeLists.txt | 13 + .../modules/partition_interface/part.cppm | 5 +- .../modules/partition_interface/primary.cppm | 5 +- .../CMakeLists.txt | 13 + .../partition_with_external_import/ext.cppm | 5 +- .../partition_with_external_import/part.cppm | 5 +- .../modules/partition_with_gmf/CMakeLists.txt | 14 + .../private_module_fragment/CMakeLists.txt | 13 + .../modules/private_module_fragment/priv.cppm | 12 +- tests/data/modules/re_export/CMakeLists.txt | 13 + tests/data/modules/re_export/core.cppm | 5 +- tests/data/modules/re_export/user.cppm | 5 +- tests/data/modules/re_export/wrapper.cppm | 5 +- .../modules/save_recompile/CMakeLists.txt | 13 + tests/data/modules/save_recompile/leaf.cppm | 5 +- tests/data/modules/save_recompile/mid.cppm | 5 +- .../single_module_no_deps/CMakeLists.txt | 13 + .../modules/single_module_no_deps/mod_a.cppm | 5 +- .../modules/template_export/CMakeLists.txt | 13 + tests/data/modules/template_export/tmpl.cppm | 14 +- .../modules/template_export/use_tmpl.cppm | 5 +- tests/integration/test_file_operation.py | 70 +-- tests/integration/test_lifecycle.py | 49 +- tests/integration/test_modules.py | 552 +++--------------- tests/integration/test_server.py | 270 ++------- tests/pyproject.toml | 14 - tests/pytest.ini | 3 + .../compile_graph_integration_tests.cpp | 5 - tests/unit/server/module_worker_tests.cpp | 61 +- tests/unit/server/stateful_worker_tests.cpp | 71 ++- tests/unit/server/stateless_worker_tests.cpp | 54 +- tests/unit/server/worker_test_helpers.h | 28 +- tests/unit/syntax/dependency_graph_tests.cpp | 6 - tests/unit/test/cdb_helper.h | 9 + 93 files changed, 1009 insertions(+), 970 deletions(-) create mode 100644 tests/data/modules/chained_modules/CMakeLists.txt create mode 100644 tests/data/modules/circular_module_dependency/CMakeLists.txt create mode 100644 tests/data/modules/class_export_and_inheritance/CMakeLists.txt create mode 100644 tests/data/modules/consumer_imports_module/CMakeLists.txt create mode 100644 tests/data/modules/deep_chain/CMakeLists.txt create mode 100644 tests/data/modules/diamond_modules/CMakeLists.txt create mode 100644 tests/data/modules/dotted_module_name/CMakeLists.txt create mode 100644 tests/data/modules/export_block/CMakeLists.txt create mode 100644 tests/data/modules/export_namespace/CMakeLists.txt create mode 100644 tests/data/modules/global_module_fragment/CMakeLists.txt create mode 100644 tests/data/modules/gmf_with_import/CMakeLists.txt create mode 100644 tests/data/modules/hover_on_imported_symbol/CMakeLists.txt create mode 100644 tests/data/modules/independent_modules/CMakeLists.txt create mode 100644 tests/data/modules/module_compile_error/CMakeLists.txt create mode 100644 tests/data/modules/module_implementation_unit/CMakeLists.txt create mode 100644 tests/data/modules/module_partitions/CMakeLists.txt create mode 100644 tests/data/modules/no_modules_plain_cpp/CMakeLists.txt create mode 100644 tests/data/modules/partition_chain/CMakeLists.txt create mode 100644 tests/data/modules/partition_interface/CMakeLists.txt create mode 100644 tests/data/modules/partition_with_external_import/CMakeLists.txt create mode 100644 tests/data/modules/partition_with_gmf/CMakeLists.txt create mode 100644 tests/data/modules/private_module_fragment/CMakeLists.txt create mode 100644 tests/data/modules/re_export/CMakeLists.txt create mode 100644 tests/data/modules/save_recompile/CMakeLists.txt create mode 100644 tests/data/modules/single_module_no_deps/CMakeLists.txt create mode 100644 tests/data/modules/template_export/CMakeLists.txt delete mode 100644 tests/pyproject.toml create mode 100644 tests/pytest.ini diff --git a/CMakeLists.txt b/CMakeLists.txt index 08b2b533..4ee13c54 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -32,12 +32,8 @@ endif() if(CMAKE_BUILD_TYPE STREQUAL "Debug") add_compile_options(-fsanitize=address) - if(NOT WIN32) - string(APPEND CMAKE_EXE_LINKER_FLAGS " -fsanitize=address") - string(APPEND CMAKE_SHARED_LINKER_FLAGS " -fsanitize=address") - endif() - - if(MSVC AND CMAKE_CXX_COMPILER_ID MATCHES "Clang") + if(CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC") + # clang-cl (MSVC frontend): manually link ASan runtime. execute_process( COMMAND ${CMAKE_CXX_COMPILER} --print-resource-dir OUTPUT_VARIABLE CLANG_RESOURCE_DIR @@ -47,7 +43,6 @@ if(CMAKE_BUILD_TYPE STREQUAL "Debug") link_directories(${ASAN_LIB_PATH}) set(ASAN_LINK_FLAGS "") - list(APPEND ASAN_LINK_FLAGS "clang_rt.asan_dynamic-x86_64.lib") list(APPEND ASAN_LINK_FLAGS "/wholearchive:clang_rt.asan_dynamic_runtime_thunk-x86_64.lib") @@ -56,6 +51,16 @@ if(CMAKE_BUILD_TYPE STREQUAL "Debug") set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} ${flag}") set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} ${flag}") endforeach() + else() + # GNU frontend (clang++/gcc): -fsanitize=address handles linking automatically. + string(APPEND CMAKE_EXE_LINKER_FLAGS " -fsanitize=address") + string(APPEND CMAKE_SHARED_LINKER_FLAGS " -fsanitize=address") + endif() + + if(WIN32) + # Disable Identical COMDAT Folding in Debug to avoid ASan ODR false positives. + string(APPEND CMAKE_EXE_LINKER_FLAGS " -Wl,/OPT:NOICF") + string(APPEND CMAKE_SHARED_LINKER_FLAGS " -Wl,/OPT:NOICF") endif() endif() diff --git a/cmake/toolchain.cmake b/cmake/toolchain.cmake index b08dd81a..67b8c18c 100644 --- a/cmake/toolchain.cmake +++ b/cmake/toolchain.cmake @@ -1,22 +1,14 @@ cmake_minimum_required(VERSION 3.30) -if(WIN32) - set(CMAKE_C_COMPILER clang-cl CACHE STRING "C compiler") - set(CMAKE_CXX_COMPILER clang-cl CACHE STRING "C++ compiler") - set(AR_PROGRAM_NAME "llvm-lib") - set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreadedDLL" CACHE STRING "MSVC runtime") +set(CMAKE_C_COMPILER clang CACHE STRING "C compiler") +set(CMAKE_CXX_COMPILER clang++ CACHE STRING "C++ compiler") +set(AR_PROGRAM_NAME "llvm-ar") +set(CMAKE_EXE_LINKER_FLAGS "-fuse-ld=lld" CACHE STRING "Executable linker flags") +set(CMAKE_SHARED_LINKER_FLAGS "-fuse-ld=lld" CACHE STRING "Shared library linker flags") +set(CMAKE_MODULE_LINKER_FLAGS "-fuse-ld=lld" CACHE STRING "Module linker flags") - find_program(LLVM_LLD_LINK_PATH "lld-link") - if(LLVM_LLD_LINK_PATH) - set(CMAKE_LINKER "${LLVM_LLD_LINK_PATH}" CACHE FILEPATH "Linker") - endif() -else() - set(CMAKE_C_COMPILER clang CACHE STRING "C compiler") - set(CMAKE_CXX_COMPILER clang++ CACHE STRING "C++ compiler") - set(AR_PROGRAM_NAME "llvm-ar") - set(CMAKE_EXE_LINKER_FLAGS "-fuse-ld=lld" CACHE STRING "Executable linker flags") - set(CMAKE_SHARED_LINKER_FLAGS "-fuse-ld=lld" CACHE STRING "Shared library linker flags") - set(CMAKE_MODULE_LINKER_FLAGS "-fuse-ld=lld" CACHE STRING "Module linker flags") +if(WIN32) + set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreadedDLL" CACHE STRING "MSVC runtime") endif() find_program(LLVM_AR_PATH ${AR_PROGRAM_NAME}) diff --git a/pixi.toml b/pixi.toml index 44a1183b..3e6a2104 100644 --- a/pixi.toml +++ b/pixi.toml @@ -194,7 +194,7 @@ prettier = "*" clang-format = "==21.1.7" [feature.format.tasks] -format-cpp = "fd -H -e cpp -e h -e cc -e hpp -x clang-format -i" +format-cpp = "fd -H -e cpp -e h -e cc -e hpp -e cppm -x clang-format -i" format-python = "ruff format ." format-lua = "stylua ." format-web = "fd -H -e js -e ts -e css -x prettier --write" diff --git a/scripts/activate_asan.bat b/scripts/activate_asan.bat index d7605f82..e4e452fc 100644 --- a/scripts/activate_asan.bat +++ b/scripts/activate_asan.bat @@ -1,3 +1,3 @@ @echo off -for /f "delims=" %%i in ('clang-cl --print-resource-dir') do set "CLANG_RES=%%i" +for /f "delims=" %%i in ('clang++ --print-resource-dir') do set "CLANG_RES=%%i" set "PATH=%CLANG_RES%\lib\windows;%PATH%" diff --git a/tests/conftest.py b/tests/conftest.py index 3a2a8b28..45db7c5b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,29 +1,34 @@ -"""Fixtures for clice LSP integration tests using pygls LanguageClient.""" +"""Fixtures and shared helpers for clice LSP integration tests using pygls LanguageClient.""" -import json import asyncio +import json +import shutil +import subprocess import sys +from collections.abc import AsyncGenerator from pathlib import Path import pytest -import pytest_asyncio from lsprotocol.types import ( PROGRESS, TEXT_DOCUMENT_PUBLISH_DIAGNOSTICS, WINDOW_WORK_DONE_PROGRESS_CREATE, ClientCapabilities, Diagnostic, + DidOpenTextDocumentParams, InitializeParams, + InitializeResult, InitializedParams, ProgressParams, PublishDiagnosticsParams, + TextDocumentItem, WorkDoneProgressCreateParams, WorkspaceFolder, ) from pygls.lsp.client import BaseLanguageClient -def pytest_addoption(parser: pytest.Parser): +def pytest_addoption(parser: pytest.Parser) -> None: parser.addoption( "--executable", required=False, @@ -53,27 +58,28 @@ def pytest_addoption(parser: pytest.Parser): class CliceClient(BaseLanguageClient): """Language client that tracks server-sent notifications.""" - def __init__(self): + def __init__(self) -> None: super().__init__("clice-test-client", "0.1.0") self.diagnostics: dict[str, list[Diagnostic]] = {} self.diagnostics_events: dict[str, asyncio.Event] = {} self.progress_tokens: list[str] = [] self.progress_events: list[dict] = [] + self.init_result: InitializeResult | None = None @self.feature(TEXT_DOCUMENT_PUBLISH_DIAGNOSTICS) - def on_diagnostics(params: PublishDiagnosticsParams): + def on_diagnostics(params: PublishDiagnosticsParams) -> None: self.diagnostics[params.uri] = list(params.diagnostics) if params.uri in self.diagnostics_events: self.diagnostics_events[params.uri].set() @self.feature(WINDOW_WORK_DONE_PROGRESS_CREATE) - def on_create_progress(params: WorkDoneProgressCreateParams): + def on_create_progress(params: WorkDoneProgressCreateParams) -> None: token = str(params.token) if isinstance(params.token, int) else params.token self.progress_tokens.append(token) return None @self.feature(PROGRESS) - def on_progress(params: ProgressParams): + def on_progress(params: ProgressParams) -> None: token = str(params.token) if isinstance(params.token, int) else params.token self.progress_events.append({"token": token, "value": params.value}) @@ -85,9 +91,56 @@ class CliceClient(BaseLanguageClient): self.diagnostics_events[uri].clear() return self.diagnostics_events[uri] + async def initialize(self, workspace: Path) -> InitializeResult: + """Initialize the LSP server with a workspace folder and return the result.""" + result = await self.initialize_async( + InitializeParams( + capabilities=ClientCapabilities(), + root_uri=workspace.as_uri(), + workspace_folders=[ + WorkspaceFolder(uri=workspace.as_uri(), name="test") + ], + ) + ) + self.initialized(InitializedParams()) + self.init_result = result + return result + + def open(self, filepath: Path, version: int = 0) -> tuple[str, str]: + """Open a text document and return (uri, content).""" + content = filepath.read_text(encoding="utf-8") + uri = filepath.as_uri() + self.text_document_did_open( + DidOpenTextDocumentParams( + text_document=TextDocumentItem( + uri=uri, language_id="cpp", version=version, text=content + ) + ) + ) + return uri, content + + async def wait_diagnostics(self, uri: str, timeout: float = 30.0) -> None: + """Wait for diagnostics on the given URI.""" + if uri in self.diagnostics: + return + event = self.wait_for_diagnostics(uri) + if uri in self.diagnostics: + return + await asyncio.wait_for(event.wait(), timeout=timeout) + + async def open_and_wait( + self, filepath: Path, timeout: float = 60.0 + ) -> tuple[str, str]: + """Open a file and wait for compilation diagnostics.""" + uri = filepath.as_uri() + event = self.wait_for_diagnostics(uri) + _, content = self.open(filepath) + await asyncio.wait_for(event.wait(), timeout=timeout) + return uri, content + @pytest.fixture(scope="session") -def executable(request) -> Path: +def executable(request: pytest.FixtureRequest) -> Path: exe = request.config.getoption("--executable") if not exe: pytest.skip("--executable not provided") @@ -108,7 +161,7 @@ def executable(request) -> Path: @pytest.fixture(scope="session") -def test_data_dir(): +def test_data_dir() -> Path: path = Path(__file__).parent / "data" data_dir = path.resolve() @@ -134,9 +187,54 @@ def test_data_dir(): return data_dir -@pytest_asyncio.fixture -async def client(request, executable: Path, test_data_dir: Path): - """Spawn clice server, yield pygls client, then shutdown+exit.""" +def generate_cdb(workspace: Path) -> None: + """Generate compile_commands.json using CMake with Ninja backend.""" + cmake = shutil.which("cmake") + if cmake is None: + raise RuntimeError("cmake executable not found in PATH") + toolchain = Path(__file__).resolve().parent.parent / "cmake" / "toolchain.cmake" + cmd = [ + cmake, + "-G", + "Ninja", + "-DCMAKE_EXPORT_COMPILE_COMMANDS=ON", + f"-DCMAKE_TOOLCHAIN_FILE={toolchain}", + "-S", + str(workspace), + "-B", + str(workspace / "build"), + ] + result = subprocess.run(cmd, capture_output=True, text=True, timeout=120) + if result.returncode != 0: + raise RuntimeError(f"cmake failed:\n{result.stderr}") + + +@pytest.fixture +def workspace(request: pytest.FixtureRequest, test_data_dir: Path) -> Path | None: + """Resolve workspace path from @pytest.mark.workspace("subdir") marker. + + If the workspace contains a CMakeLists.txt, automatically runs cmake + to generate compile_commands.json. Returns None if no marker is present. + """ + marker = request.node.get_closest_marker("workspace") + if marker is None: + return None + if not marker.args or not isinstance(marker.args[0], str): + raise pytest.UsageError( + "@pytest.mark.workspace requires a string argument, e.g. " + '@pytest.mark.workspace("modules/hello_world")' + ) + path = test_data_dir / marker.args[0] + if (path / "CMakeLists.txt").exists(): + generate_cdb(path) + return path + + +@pytest.fixture +async def client( + request: pytest.FixtureRequest, executable: Path, workspace: Path | None +): + """Spawn clice server, auto-initialize if @pytest.mark.workspace is present.""" config = request.config mode = config.getoption("--mode") @@ -149,6 +247,9 @@ async def client(request, executable: Path, test_data_dir: Path): c = CliceClient() await c.start_io(*cmd) + if workspace is not None: + await c.initialize(workspace) + yield c # Graceful shutdown diff --git a/tests/data/modules/chained_modules/CMakeLists.txt b/tests/data/modules/chained_modules/CMakeLists.txt new file mode 100644 index 00000000..566af29f --- /dev/null +++ b/tests/data/modules/chained_modules/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.28) +project(chained_modules LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +add_library(test) +target_sources(test + PUBLIC + FILE_SET CXX_MODULES + FILES mod_a.cppm mod_b.cppm +) + diff --git a/tests/data/modules/chained_modules/mod_a.cppm b/tests/data/modules/chained_modules/mod_a.cppm index 9823e0be..0e8e8d1d 100644 --- a/tests/data/modules/chained_modules/mod_a.cppm +++ b/tests/data/modules/chained_modules/mod_a.cppm @@ -1,2 +1,5 @@ export module A; -export int foo() { return 42; } + +export int foo() { + return 42; +} diff --git a/tests/data/modules/chained_modules/mod_b.cppm b/tests/data/modules/chained_modules/mod_b.cppm index 50adc1f4..054edab1 100644 --- a/tests/data/modules/chained_modules/mod_b.cppm +++ b/tests/data/modules/chained_modules/mod_b.cppm @@ -1,3 +1,6 @@ export module B; import A; -export int bar() { return foo() + 1; } + +export int bar() { + return foo() + 1; +} diff --git a/tests/data/modules/circular_module_dependency/CMakeLists.txt b/tests/data/modules/circular_module_dependency/CMakeLists.txt new file mode 100644 index 00000000..157ab849 --- /dev/null +++ b/tests/data/modules/circular_module_dependency/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.28) +project(circular_module_dependency LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +add_library(test) +target_sources(test + PUBLIC + FILE_SET CXX_MODULES + FILES cycle_a.cppm cycle_b.cppm ok.cppm +) + diff --git a/tests/data/modules/circular_module_dependency/cycle_a.cppm b/tests/data/modules/circular_module_dependency/cycle_a.cppm index 91f74918..84af9b06 100644 --- a/tests/data/modules/circular_module_dependency/cycle_a.cppm +++ b/tests/data/modules/circular_module_dependency/cycle_a.cppm @@ -1,3 +1,6 @@ export module CycA; import CycB; -export int a() { return 1; } + +export int a() { + return 1; +} diff --git a/tests/data/modules/circular_module_dependency/cycle_b.cppm b/tests/data/modules/circular_module_dependency/cycle_b.cppm index 088c3cc2..e0b843e3 100644 --- a/tests/data/modules/circular_module_dependency/cycle_b.cppm +++ b/tests/data/modules/circular_module_dependency/cycle_b.cppm @@ -1,3 +1,6 @@ export module CycB; import CycA; -export int b() { return 2; } + +export int b() { + return 2; +} diff --git a/tests/data/modules/circular_module_dependency/ok.cppm b/tests/data/modules/circular_module_dependency/ok.cppm index 00727c1e..130e99b8 100644 --- a/tests/data/modules/circular_module_dependency/ok.cppm +++ b/tests/data/modules/circular_module_dependency/ok.cppm @@ -1,2 +1,5 @@ export module Ok; -export int ok() { return 42; } + +export int ok() { + return 42; +} diff --git a/tests/data/modules/class_export_and_inheritance/CMakeLists.txt b/tests/data/modules/class_export_and_inheritance/CMakeLists.txt new file mode 100644 index 00000000..825cba88 --- /dev/null +++ b/tests/data/modules/class_export_and_inheritance/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.28) +project(class_export_and_inheritance LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +add_library(test) +target_sources(test + PUBLIC + FILE_SET CXX_MODULES + FILES shape.cppm circle.cppm +) + diff --git a/tests/data/modules/class_export_and_inheritance/circle.cppm b/tests/data/modules/class_export_and_inheritance/circle.cppm index d6e79259..f0e2a135 100644 --- a/tests/data/modules/class_export_and_inheritance/circle.cppm +++ b/tests/data/modules/class_export_and_inheritance/circle.cppm @@ -1,8 +1,13 @@ export module Circle; import Shape; + export class Circle : public Shape { int r; + public: Circle(int r) : r(r) {} - int area() const override { return 3 * r * r; } + + int area() const override { + return 3 * r * r; + } }; diff --git a/tests/data/modules/class_export_and_inheritance/shape.cppm b/tests/data/modules/class_export_and_inheritance/shape.cppm index dbda3b12..357072bc 100644 --- a/tests/data/modules/class_export_and_inheritance/shape.cppm +++ b/tests/data/modules/class_export_and_inheritance/shape.cppm @@ -1,4 +1,5 @@ export module Shape; + export class Shape { public: virtual ~Shape() = default; diff --git a/tests/data/modules/consumer_imports_module/CMakeLists.txt b/tests/data/modules/consumer_imports_module/CMakeLists.txt new file mode 100644 index 00000000..2355801f --- /dev/null +++ b/tests/data/modules/consumer_imports_module/CMakeLists.txt @@ -0,0 +1,14 @@ +cmake_minimum_required(VERSION 3.28) +project(consumer_imports_module LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +add_library(test) +target_sources(test + PUBLIC + FILE_SET CXX_MODULES + FILES math.cppm +) +target_sources(test PRIVATE main.cpp) + diff --git a/tests/data/modules/consumer_imports_module/math.cppm b/tests/data/modules/consumer_imports_module/math.cppm index 665bb79d..d76b88c6 100644 --- a/tests/data/modules/consumer_imports_module/math.cppm +++ b/tests/data/modules/consumer_imports_module/math.cppm @@ -1,2 +1,5 @@ export module Math; -export int add(int a, int b) { return a + b; } + +export int add(int a, int b) { + return a + b; +} diff --git a/tests/data/modules/deep_chain/CMakeLists.txt b/tests/data/modules/deep_chain/CMakeLists.txt new file mode 100644 index 00000000..a8379308 --- /dev/null +++ b/tests/data/modules/deep_chain/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.28) +project(deep_chain LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +add_library(test) +target_sources(test + PUBLIC + FILE_SET CXX_MODULES + FILES m1.cppm m2.cppm m3.cppm m4.cppm m5.cppm +) + diff --git a/tests/data/modules/deep_chain/m1.cppm b/tests/data/modules/deep_chain/m1.cppm index 56fe2cf4..dc2ada4b 100644 --- a/tests/data/modules/deep_chain/m1.cppm +++ b/tests/data/modules/deep_chain/m1.cppm @@ -1,2 +1,5 @@ export module M1; -export int f1() { return 1; } + +export int f1() { + return 1; +} diff --git a/tests/data/modules/deep_chain/m2.cppm b/tests/data/modules/deep_chain/m2.cppm index 5393ba1c..93726e05 100644 --- a/tests/data/modules/deep_chain/m2.cppm +++ b/tests/data/modules/deep_chain/m2.cppm @@ -1,3 +1,6 @@ export module M2; import M1; -export int f2() { return f1() + 1; } + +export int f2() { + return f1() + 1; +} diff --git a/tests/data/modules/deep_chain/m3.cppm b/tests/data/modules/deep_chain/m3.cppm index 6c253295..195c61e5 100644 --- a/tests/data/modules/deep_chain/m3.cppm +++ b/tests/data/modules/deep_chain/m3.cppm @@ -1,3 +1,6 @@ export module M3; import M2; -export int f3() { return f2() + 1; } + +export int f3() { + return f2() + 1; +} diff --git a/tests/data/modules/deep_chain/m4.cppm b/tests/data/modules/deep_chain/m4.cppm index 081b9919..00c1f986 100644 --- a/tests/data/modules/deep_chain/m4.cppm +++ b/tests/data/modules/deep_chain/m4.cppm @@ -1,3 +1,6 @@ export module M4; import M3; -export int f4() { return f3() + 1; } + +export int f4() { + return f3() + 1; +} diff --git a/tests/data/modules/deep_chain/m5.cppm b/tests/data/modules/deep_chain/m5.cppm index f5cae81a..0adbfdbd 100644 --- a/tests/data/modules/deep_chain/m5.cppm +++ b/tests/data/modules/deep_chain/m5.cppm @@ -1,3 +1,6 @@ export module M5; import M4; -export int f5() { return f4() + 1; } + +export int f5() { + return f4() + 1; +} diff --git a/tests/data/modules/diamond_modules/CMakeLists.txt b/tests/data/modules/diamond_modules/CMakeLists.txt new file mode 100644 index 00000000..74cc4aed --- /dev/null +++ b/tests/data/modules/diamond_modules/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.28) +project(diamond_modules LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +add_library(test) +target_sources(test + PUBLIC + FILE_SET CXX_MODULES + FILES base.cppm left.cppm right.cppm top.cppm +) + diff --git a/tests/data/modules/diamond_modules/base.cppm b/tests/data/modules/diamond_modules/base.cppm index afb06de7..a057a138 100644 --- a/tests/data/modules/diamond_modules/base.cppm +++ b/tests/data/modules/diamond_modules/base.cppm @@ -1,2 +1,5 @@ export module Base; -export int base_val() { return 10; } + +export int base_val() { + return 10; +} diff --git a/tests/data/modules/diamond_modules/left.cppm b/tests/data/modules/diamond_modules/left.cppm index 48b7329d..684e8294 100644 --- a/tests/data/modules/diamond_modules/left.cppm +++ b/tests/data/modules/diamond_modules/left.cppm @@ -1,3 +1,6 @@ export module Left; import Base; -export int left_val() { return base_val() + 1; } + +export int left_val() { + return base_val() + 1; +} diff --git a/tests/data/modules/diamond_modules/right.cppm b/tests/data/modules/diamond_modules/right.cppm index 832452e9..7815c43c 100644 --- a/tests/data/modules/diamond_modules/right.cppm +++ b/tests/data/modules/diamond_modules/right.cppm @@ -1,3 +1,6 @@ export module Right; import Base; -export int right_val() { return base_val() + 2; } + +export int right_val() { + return base_val() + 2; +} diff --git a/tests/data/modules/diamond_modules/top.cppm b/tests/data/modules/diamond_modules/top.cppm index 0290c0e6..8a212a5d 100644 --- a/tests/data/modules/diamond_modules/top.cppm +++ b/tests/data/modules/diamond_modules/top.cppm @@ -1,4 +1,7 @@ export module Top; import Left; import Right; -export int top_val() { return left_val() + right_val(); } + +export int top_val() { + return left_val() + right_val(); +} diff --git a/tests/data/modules/dotted_module_name/CMakeLists.txt b/tests/data/modules/dotted_module_name/CMakeLists.txt new file mode 100644 index 00000000..f762b023 --- /dev/null +++ b/tests/data/modules/dotted_module_name/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.28) +project(dotted_module_name LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +add_library(test) +target_sources(test + PUBLIC + FILE_SET CXX_MODULES + FILES io.cppm app.cppm +) + diff --git a/tests/data/modules/dotted_module_name/app.cppm b/tests/data/modules/dotted_module_name/app.cppm index 3f6e0cdb..ecc7eda8 100644 --- a/tests/data/modules/dotted_module_name/app.cppm +++ b/tests/data/modules/dotted_module_name/app.cppm @@ -1,3 +1,6 @@ export module my.app; import my.io; -export void run() { print(); } + +export void run() { + print(); +} diff --git a/tests/data/modules/dotted_module_name/io.cppm b/tests/data/modules/dotted_module_name/io.cppm index 384ef799..bf222fcb 100644 --- a/tests/data/modules/dotted_module_name/io.cppm +++ b/tests/data/modules/dotted_module_name/io.cppm @@ -1,2 +1,3 @@ export module my.io; + export void print() {} diff --git a/tests/data/modules/export_block/CMakeLists.txt b/tests/data/modules/export_block/CMakeLists.txt new file mode 100644 index 00000000..131f2050 --- /dev/null +++ b/tests/data/modules/export_block/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.28) +project(export_block LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +add_library(test) +target_sources(test + PUBLIC + FILE_SET CXX_MODULES + FILES block.cppm consumer.cppm +) + diff --git a/tests/data/modules/export_block/block.cppm b/tests/data/modules/export_block/block.cppm index 4c050f78..390d7c8a 100644 --- a/tests/data/modules/export_block/block.cppm +++ b/tests/data/modules/export_block/block.cppm @@ -1,8 +1,18 @@ export module Block; export { - int alpha() { return 1; } - int beta() { return 2; } - namespace ns { - int gamma() { return 3; } + int alpha() { + return 1; } + + int beta() { + return 2; + } + + namespace ns { + + int gamma() { + return 3; + } + + } // namespace ns } diff --git a/tests/data/modules/export_block/consumer.cppm b/tests/data/modules/export_block/consumer.cppm index 9cfc6de3..55fbd4e3 100644 --- a/tests/data/modules/export_block/consumer.cppm +++ b/tests/data/modules/export_block/consumer.cppm @@ -1,3 +1,6 @@ export module Consumer; import Block; -export int total() { return alpha() + beta() + ns::gamma(); } + +export int total() { + return alpha() + beta() + ns::gamma(); +} diff --git a/tests/data/modules/export_namespace/CMakeLists.txt b/tests/data/modules/export_namespace/CMakeLists.txt new file mode 100644 index 00000000..65caba5f --- /dev/null +++ b/tests/data/modules/export_namespace/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.28) +project(export_namespace LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +add_library(test) +target_sources(test + PUBLIC + FILE_SET CXX_MODULES + FILES ns.cppm calc.cppm +) + diff --git a/tests/data/modules/export_namespace/calc.cppm b/tests/data/modules/export_namespace/calc.cppm index 9c38fdfa..fe96cc68 100644 --- a/tests/data/modules/export_namespace/calc.cppm +++ b/tests/data/modules/export_namespace/calc.cppm @@ -1,3 +1,6 @@ export module Calc; import NS; -export int compute() { return math::add(3, math::mul(4, 5)); } + +export int compute() { + return math::add(3, math::mul(4, 5)); +} diff --git a/tests/data/modules/export_namespace/ns.cppm b/tests/data/modules/export_namespace/ns.cppm index c7e8f8d9..3c38707e 100644 --- a/tests/data/modules/export_namespace/ns.cppm +++ b/tests/data/modules/export_namespace/ns.cppm @@ -1,5 +1,13 @@ export module NS; + export namespace math { - int add(int a, int b) { return a + b; } - int mul(int a, int b) { return a * b; } + +int add(int a, int b) { + return a + b; } + +int mul(int a, int b) { + return a * b; +} + +} // namespace math diff --git a/tests/data/modules/global_module_fragment/CMakeLists.txt b/tests/data/modules/global_module_fragment/CMakeLists.txt new file mode 100644 index 00000000..80fec077 --- /dev/null +++ b/tests/data/modules/global_module_fragment/CMakeLists.txt @@ -0,0 +1,14 @@ +cmake_minimum_required(VERSION 3.28) +project(global_module_fragment LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +add_library(test) +target_sources(test + PUBLIC + FILE_SET CXX_MODULES + FILES gmf.cppm +) +target_include_directories(test PRIVATE ${CMAKE_SOURCE_DIR}) + diff --git a/tests/data/modules/global_module_fragment/gmf.cppm b/tests/data/modules/global_module_fragment/gmf.cppm index 31308b95..2a207e72 100644 --- a/tests/data/modules/global_module_fragment/gmf.cppm +++ b/tests/data/modules/global_module_fragment/gmf.cppm @@ -1,4 +1,7 @@ module; #include "legacy.h" export module GMF; -export int wrapped() { return legacy_fn(); } + +export int wrapped() { + return legacy_fn(); +} diff --git a/tests/data/modules/gmf_with_import/CMakeLists.txt b/tests/data/modules/gmf_with_import/CMakeLists.txt new file mode 100644 index 00000000..08d645af --- /dev/null +++ b/tests/data/modules/gmf_with_import/CMakeLists.txt @@ -0,0 +1,14 @@ +cmake_minimum_required(VERSION 3.28) +project(gmf_with_import LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +add_library(test) +target_sources(test + PUBLIC + FILE_SET CXX_MODULES + FILES base.cppm combined.cppm +) +target_include_directories(test PRIVATE ${CMAKE_SOURCE_DIR}) + diff --git a/tests/data/modules/gmf_with_import/base.cppm b/tests/data/modules/gmf_with_import/base.cppm index 23950098..adcf29a3 100644 --- a/tests/data/modules/gmf_with_import/base.cppm +++ b/tests/data/modules/gmf_with_import/base.cppm @@ -1,2 +1,5 @@ export module Base; -export int base() { return 100; } + +export int base() { + return 100; +} diff --git a/tests/data/modules/gmf_with_import/combined.cppm b/tests/data/modules/gmf_with_import/combined.cppm index 4fc4b50d..431742ae 100644 --- a/tests/data/modules/gmf_with_import/combined.cppm +++ b/tests/data/modules/gmf_with_import/combined.cppm @@ -2,4 +2,7 @@ module; #include "util.h" export module Combined; import Base; -export int combined() { return base() + util_helper(); } + +export int combined() { + return base() + util_helper(); +} diff --git a/tests/data/modules/hover_on_imported_symbol/CMakeLists.txt b/tests/data/modules/hover_on_imported_symbol/CMakeLists.txt new file mode 100644 index 00000000..df997a02 --- /dev/null +++ b/tests/data/modules/hover_on_imported_symbol/CMakeLists.txt @@ -0,0 +1,14 @@ +cmake_minimum_required(VERSION 3.28) +project(hover_on_imported_symbol LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +add_library(test) +target_sources(test + PUBLIC + FILE_SET CXX_MODULES + FILES defs.cppm +) +target_sources(test PRIVATE use.cpp) + diff --git a/tests/data/modules/hover_on_imported_symbol/defs.cppm b/tests/data/modules/hover_on_imported_symbol/defs.cppm index 23c27ecc..4858e890 100644 --- a/tests/data/modules/hover_on_imported_symbol/defs.cppm +++ b/tests/data/modules/hover_on_imported_symbol/defs.cppm @@ -1,2 +1,5 @@ export module Defs; -export int magic_number() { return 42; } + +export int magic_number() { + return 42; +} diff --git a/tests/data/modules/independent_modules/CMakeLists.txt b/tests/data/modules/independent_modules/CMakeLists.txt new file mode 100644 index 00000000..3c2f18ac --- /dev/null +++ b/tests/data/modules/independent_modules/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.28) +project(independent_modules LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +add_library(test) +target_sources(test + PUBLIC + FILE_SET CXX_MODULES + FILES x.cppm y.cppm +) + diff --git a/tests/data/modules/independent_modules/x.cppm b/tests/data/modules/independent_modules/x.cppm index 5a8542f5..a3708aca 100644 --- a/tests/data/modules/independent_modules/x.cppm +++ b/tests/data/modules/independent_modules/x.cppm @@ -1,2 +1,5 @@ export module X; -export int x() { return 1; } + +export int x() { + return 1; +} diff --git a/tests/data/modules/independent_modules/y.cppm b/tests/data/modules/independent_modules/y.cppm index 37553cdc..2eee1339 100644 --- a/tests/data/modules/independent_modules/y.cppm +++ b/tests/data/modules/independent_modules/y.cppm @@ -1,2 +1,5 @@ export module Y; -export int y() { return 2; } + +export int y() { + return 2; +} diff --git a/tests/data/modules/module_compile_error/CMakeLists.txt b/tests/data/modules/module_compile_error/CMakeLists.txt new file mode 100644 index 00000000..cbfbba47 --- /dev/null +++ b/tests/data/modules/module_compile_error/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.28) +project(module_compile_error LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +add_library(test) +target_sources(test + PUBLIC + FILE_SET CXX_MODULES + FILES good.cppm bad.cppm +) + diff --git a/tests/data/modules/module_compile_error/bad.cppm b/tests/data/modules/module_compile_error/bad.cppm index 1cf65f37..2c2d07bb 100644 --- a/tests/data/modules/module_compile_error/bad.cppm +++ b/tests/data/modules/module_compile_error/bad.cppm @@ -1,3 +1,6 @@ export module Bad; import Good; -export int bad() { return UNDEFINED_SYMBOL; } + +export int bad() { + return UNDEFINED_SYMBOL; +} diff --git a/tests/data/modules/module_compile_error/good.cppm b/tests/data/modules/module_compile_error/good.cppm index 43e3b993..fbde230d 100644 --- a/tests/data/modules/module_compile_error/good.cppm +++ b/tests/data/modules/module_compile_error/good.cppm @@ -1,2 +1,5 @@ export module Good; -export int good() { return 1; } + +export int good() { + return 1; +} diff --git a/tests/data/modules/module_implementation_unit/CMakeLists.txt b/tests/data/modules/module_implementation_unit/CMakeLists.txt new file mode 100644 index 00000000..4c367012 --- /dev/null +++ b/tests/data/modules/module_implementation_unit/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.28) +project(module_implementation_unit LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +add_library(test) +target_sources(test + PUBLIC + FILE_SET CXX_MODULES + FILES greeter.cppm greeter_impl.cpp +) + diff --git a/tests/data/modules/module_partitions/CMakeLists.txt b/tests/data/modules/module_partitions/CMakeLists.txt new file mode 100644 index 00000000..0153707c --- /dev/null +++ b/tests/data/modules/module_partitions/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.28) +project(module_partitions LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +add_library(test) +target_sources(test + PUBLIC + FILE_SET CXX_MODULES + FILES part_a.cppm part_b.cppm lib.cppm +) + diff --git a/tests/data/modules/module_partitions/lib.cppm b/tests/data/modules/module_partitions/lib.cppm index 0472f8ae..fd7c5d9d 100644 --- a/tests/data/modules/module_partitions/lib.cppm +++ b/tests/data/modules/module_partitions/lib.cppm @@ -1,4 +1,7 @@ export module Lib; export import :A; export import :B; -export int lib_fn() { return a_fn() + b_fn(); } + +export int lib_fn() { + return a_fn() + b_fn(); +} diff --git a/tests/data/modules/module_partitions/part_a.cppm b/tests/data/modules/module_partitions/part_a.cppm index 1152f497..84d8d3f9 100644 --- a/tests/data/modules/module_partitions/part_a.cppm +++ b/tests/data/modules/module_partitions/part_a.cppm @@ -1,2 +1,5 @@ export module Lib:A; -export int a_fn() { return 1; } + +export int a_fn() { + return 1; +} diff --git a/tests/data/modules/module_partitions/part_b.cppm b/tests/data/modules/module_partitions/part_b.cppm index 731a9e50..e8a7ce9b 100644 --- a/tests/data/modules/module_partitions/part_b.cppm +++ b/tests/data/modules/module_partitions/part_b.cppm @@ -1,2 +1,5 @@ export module Lib:B; -export int b_fn() { return 2; } + +export int b_fn() { + return 2; +} diff --git a/tests/data/modules/no_modules_plain_cpp/CMakeLists.txt b/tests/data/modules/no_modules_plain_cpp/CMakeLists.txt new file mode 100644 index 00000000..3449fd7e --- /dev/null +++ b/tests/data/modules/no_modules_plain_cpp/CMakeLists.txt @@ -0,0 +1,8 @@ +cmake_minimum_required(VERSION 3.28) +project(no_modules_plain_cpp LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +add_library(test STATIC plain.cpp) + diff --git a/tests/data/modules/partition_chain/CMakeLists.txt b/tests/data/modules/partition_chain/CMakeLists.txt new file mode 100644 index 00000000..ceb3d1ed --- /dev/null +++ b/tests/data/modules/partition_chain/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.28) +project(partition_chain LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +add_library(test) +target_sources(test + PUBLIC + FILE_SET CXX_MODULES + FILES types.cppm core.cppm sys.cppm +) + diff --git a/tests/data/modules/partition_chain/core.cppm b/tests/data/modules/partition_chain/core.cppm index e2ad946e..9e24941a 100644 --- a/tests/data/modules/partition_chain/core.cppm +++ b/tests/data/modules/partition_chain/core.cppm @@ -1,3 +1,6 @@ export module Sys:Core; import :Types; -export Config make_config() { return {42}; } + +export Config make_config() { + return {42}; +} diff --git a/tests/data/modules/partition_chain/types.cppm b/tests/data/modules/partition_chain/types.cppm index 3c277808..2037deb3 100644 --- a/tests/data/modules/partition_chain/types.cppm +++ b/tests/data/modules/partition_chain/types.cppm @@ -1,2 +1,5 @@ export module Sys:Types; -export struct Config { int value = 0; }; + +export struct Config { + int value = 0; +}; diff --git a/tests/data/modules/partition_interface/CMakeLists.txt b/tests/data/modules/partition_interface/CMakeLists.txt new file mode 100644 index 00000000..e525d2ea --- /dev/null +++ b/tests/data/modules/partition_interface/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.28) +project(partition_interface LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +add_library(test) +target_sources(test + PUBLIC + FILE_SET CXX_MODULES + FILES part.cppm primary.cppm +) + diff --git a/tests/data/modules/partition_interface/part.cppm b/tests/data/modules/partition_interface/part.cppm index be60dfd1..f192f507 100644 --- a/tests/data/modules/partition_interface/part.cppm +++ b/tests/data/modules/partition_interface/part.cppm @@ -1,2 +1,5 @@ export module M:Part; -export int part_fn() { return 5; } + +export int part_fn() { + return 5; +} diff --git a/tests/data/modules/partition_interface/primary.cppm b/tests/data/modules/partition_interface/primary.cppm index b022380c..9f51dbdd 100644 --- a/tests/data/modules/partition_interface/primary.cppm +++ b/tests/data/modules/partition_interface/primary.cppm @@ -1,3 +1,6 @@ export module M; export import :Part; -export int primary_fn() { return part_fn() + 1; } + +export int primary_fn() { + return part_fn() + 1; +} diff --git a/tests/data/modules/partition_with_external_import/CMakeLists.txt b/tests/data/modules/partition_with_external_import/CMakeLists.txt new file mode 100644 index 00000000..4f344559 --- /dev/null +++ b/tests/data/modules/partition_with_external_import/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.28) +project(partition_with_external_import LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +add_library(test) +target_sources(test + PUBLIC + FILE_SET CXX_MODULES + FILES ext.cppm part.cppm app.cppm +) + diff --git a/tests/data/modules/partition_with_external_import/ext.cppm b/tests/data/modules/partition_with_external_import/ext.cppm index 9b5c8d02..f2695ec8 100644 --- a/tests/data/modules/partition_with_external_import/ext.cppm +++ b/tests/data/modules/partition_with_external_import/ext.cppm @@ -1,2 +1,5 @@ export module Ext; -export int ext_val() { return 99; } + +export int ext_val() { + return 99; +} diff --git a/tests/data/modules/partition_with_external_import/part.cppm b/tests/data/modules/partition_with_external_import/part.cppm index 1920565d..bd7fd198 100644 --- a/tests/data/modules/partition_with_external_import/part.cppm +++ b/tests/data/modules/partition_with_external_import/part.cppm @@ -1,3 +1,6 @@ export module App:Core; import Ext; -export int core_fn() { return ext_val() + 1; } + +export int core_fn() { + return ext_val() + 1; +} diff --git a/tests/data/modules/partition_with_gmf/CMakeLists.txt b/tests/data/modules/partition_with_gmf/CMakeLists.txt new file mode 100644 index 00000000..18a62671 --- /dev/null +++ b/tests/data/modules/partition_with_gmf/CMakeLists.txt @@ -0,0 +1,14 @@ +cmake_minimum_required(VERSION 3.28) +project(partition_with_gmf LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +add_library(test) +target_sources(test + PUBLIC + FILE_SET CXX_MODULES + FILES part_cfg.cppm cfg.cppm +) +target_include_directories(test PRIVATE ${CMAKE_SOURCE_DIR}) + diff --git a/tests/data/modules/private_module_fragment/CMakeLists.txt b/tests/data/modules/private_module_fragment/CMakeLists.txt new file mode 100644 index 00000000..9e4d36d2 --- /dev/null +++ b/tests/data/modules/private_module_fragment/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.28) +project(private_module_fragment LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +add_library(test) +target_sources(test + PUBLIC + FILE_SET CXX_MODULES + FILES priv.cppm +) + diff --git a/tests/data/modules/private_module_fragment/priv.cppm b/tests/data/modules/private_module_fragment/priv.cppm index 7acc46d9..0b46cf5e 100644 --- a/tests/data/modules/private_module_fragment/priv.cppm +++ b/tests/data/modules/private_module_fragment/priv.cppm @@ -1,5 +1,11 @@ export module Priv; export int public_fn(); -module : private; -int public_fn() { return 42; } -int private_helper() { return 7; } +module :private; + +int public_fn() { + return 42; +} + +int private_helper() { + return 7; +} diff --git a/tests/data/modules/re_export/CMakeLists.txt b/tests/data/modules/re_export/CMakeLists.txt new file mode 100644 index 00000000..76724dc4 --- /dev/null +++ b/tests/data/modules/re_export/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.28) +project(re_export LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +add_library(test) +target_sources(test + PUBLIC + FILE_SET CXX_MODULES + FILES core.cppm wrapper.cppm user.cppm +) + diff --git a/tests/data/modules/re_export/core.cppm b/tests/data/modules/re_export/core.cppm index cdffe208..03089bc7 100644 --- a/tests/data/modules/re_export/core.cppm +++ b/tests/data/modules/re_export/core.cppm @@ -1,2 +1,5 @@ export module Core; -export int core_fn() { return 1; } + +export int core_fn() { + return 1; +} diff --git a/tests/data/modules/re_export/user.cppm b/tests/data/modules/re_export/user.cppm index bfa42f3b..8f5c075a 100644 --- a/tests/data/modules/re_export/user.cppm +++ b/tests/data/modules/re_export/user.cppm @@ -1,3 +1,6 @@ export module User; import Wrapper; -export int use_fn() { return core_fn() + wrap_fn(); } + +export int use_fn() { + return core_fn() + wrap_fn(); +} diff --git a/tests/data/modules/re_export/wrapper.cppm b/tests/data/modules/re_export/wrapper.cppm index 2d9c6039..784e45d3 100644 --- a/tests/data/modules/re_export/wrapper.cppm +++ b/tests/data/modules/re_export/wrapper.cppm @@ -1,3 +1,6 @@ export module Wrapper; export import Core; -export int wrap_fn() { return core_fn() + 10; } + +export int wrap_fn() { + return core_fn() + 10; +} diff --git a/tests/data/modules/save_recompile/CMakeLists.txt b/tests/data/modules/save_recompile/CMakeLists.txt new file mode 100644 index 00000000..77d2609f --- /dev/null +++ b/tests/data/modules/save_recompile/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.28) +project(save_recompile LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +add_library(test) +target_sources(test + PUBLIC + FILE_SET CXX_MODULES + FILES leaf.cppm mid.cppm +) + diff --git a/tests/data/modules/save_recompile/leaf.cppm b/tests/data/modules/save_recompile/leaf.cppm index 17c57faa..509e65cf 100644 --- a/tests/data/modules/save_recompile/leaf.cppm +++ b/tests/data/modules/save_recompile/leaf.cppm @@ -1,2 +1,5 @@ export module Leaf; -export int leaf() { return 1; } + +export int leaf() { + return 1; +} diff --git a/tests/data/modules/save_recompile/mid.cppm b/tests/data/modules/save_recompile/mid.cppm index 2c306523..52e49c01 100644 --- a/tests/data/modules/save_recompile/mid.cppm +++ b/tests/data/modules/save_recompile/mid.cppm @@ -1,3 +1,6 @@ export module Mid; import Leaf; -export int mid() { return leaf() + 1; } + +export int mid() { + return leaf() + 1; +} diff --git a/tests/data/modules/single_module_no_deps/CMakeLists.txt b/tests/data/modules/single_module_no_deps/CMakeLists.txt new file mode 100644 index 00000000..35e0cc67 --- /dev/null +++ b/tests/data/modules/single_module_no_deps/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.28) +project(single_module_no_deps LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +add_library(test) +target_sources(test + PUBLIC + FILE_SET CXX_MODULES + FILES mod_a.cppm +) + diff --git a/tests/data/modules/single_module_no_deps/mod_a.cppm b/tests/data/modules/single_module_no_deps/mod_a.cppm index 9823e0be..0e8e8d1d 100644 --- a/tests/data/modules/single_module_no_deps/mod_a.cppm +++ b/tests/data/modules/single_module_no_deps/mod_a.cppm @@ -1,2 +1,5 @@ export module A; -export int foo() { return 42; } + +export int foo() { + return 42; +} diff --git a/tests/data/modules/template_export/CMakeLists.txt b/tests/data/modules/template_export/CMakeLists.txt new file mode 100644 index 00000000..11813986 --- /dev/null +++ b/tests/data/modules/template_export/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.28) +project(template_export LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +add_library(test) +target_sources(test + PUBLIC + FILE_SET CXX_MODULES + FILES tmpl.cppm use_tmpl.cppm +) + diff --git a/tests/data/modules/template_export/tmpl.cppm b/tests/data/modules/template_export/tmpl.cppm index afed2ed0..872c1538 100644 --- a/tests/data/modules/template_export/tmpl.cppm +++ b/tests/data/modules/template_export/tmpl.cppm @@ -1,5 +1,11 @@ export module Tmpl; -export template -T identity(T x) { return x; } -export template -auto pair_sum(T a, U b) { return a + b; } + +export template +T identity(T x) { + return x; +} + +export template +auto pair_sum(T a, U b) { + return a + b; +} diff --git a/tests/data/modules/template_export/use_tmpl.cppm b/tests/data/modules/template_export/use_tmpl.cppm index 427bb1c4..272fe992 100644 --- a/tests/data/modules/template_export/use_tmpl.cppm +++ b/tests/data/modules/template_export/use_tmpl.cppm @@ -1,3 +1,6 @@ export module UseTmpl; import Tmpl; -export int test() { return identity(42) + pair_sum(1, 2); } + +export int test() { + return identity(42) + pair_sum(1, 2); +} diff --git a/tests/integration/test_file_operation.py b/tests/integration/test_file_operation.py index 07b782e0..cb452e7b 100644 --- a/tests/integration/test_file_operation.py +++ b/tests/integration/test_file_operation.py @@ -1,68 +1,31 @@ -"""File operation tests for the clice LSP server using pygls.""" +"""File operation tests for the clice LSP server.""" import asyncio import pytest from lsprotocol.types import ( - ClientCapabilities, CompletionParams, DidChangeTextDocumentParams, DidCloseTextDocumentParams, - DidOpenTextDocumentParams, DidSaveTextDocumentParams, HoverParams, - InitializeParams, - InitializedParams, Position, SignatureHelpParams, TextDocumentContentChangeWholeDocument, TextDocumentIdentifier, - TextDocumentItem, VersionedTextDocumentIdentifier, - WorkspaceFolder, ) -async def _init(client, workspace): - await client.initialize_async( - InitializeParams( - capabilities=ClientCapabilities(), - root_uri=workspace.as_uri(), - workspace_folders=[WorkspaceFolder(uri=workspace.as_uri(), name="test")], - ) - ) - client.initialized(InitializedParams()) - - -def _open(client, path): - uri = path.as_uri() - content = path.read_text(encoding="utf-8") - client.text_document_did_open( - DidOpenTextDocumentParams( - text_document=TextDocumentItem( - uri=uri, - language_id="cpp", - version=0, - text=content, - ) - ) - ) - return uri, content - - -@pytest.mark.asyncio -async def test_did_open(client, test_data_dir): - workspace = test_data_dir / "hello_world" - await _init(client, workspace) - _open(client, workspace / "main.cpp") +@pytest.mark.workspace("hello_world") +async def test_did_open(client, workspace): + client.open(workspace / "main.cpp") await asyncio.sleep(5) -@pytest.mark.asyncio -async def test_did_change(client, test_data_dir): - workspace = test_data_dir / "hello_world" - await _init(client, workspace) - uri, content = _open(client, workspace / "main.cpp") +@pytest.mark.workspace("hello_world") +async def test_did_change(client, workspace): + uri, content = client.open(workspace / "main.cpp") for i in range(20): content += "\n" @@ -76,25 +39,20 @@ async def test_did_change(client, test_data_dir): await asyncio.sleep(5) -@pytest.mark.asyncio -async def test_clang_tidy(client, test_data_dir): - workspace = test_data_dir / "clang_tidy" - await _init(client, workspace) - _open(client, workspace / "main.cpp") +@pytest.mark.workspace("clang_tidy") +async def test_clang_tidy(client, workspace): + client.open(workspace / "main.cpp") await asyncio.sleep(5) -@pytest.mark.asyncio -async def test_hover_save_close(client, test_data_dir): - workspace = test_data_dir / "hello_world" +@pytest.mark.workspace("hello_world") +async def test_hover_save_close(client, workspace): main_cpp = workspace / "main.cpp" - await _init(client, workspace) - uri, content = _open(client, main_cpp) + uri, content = client.open(main_cpp) # Wait for initial compilation - event = client.wait_for_diagnostics(uri) - await asyncio.wait_for(event.wait(), timeout=30.0) + await client.wait_diagnostics(uri) # Change and save content += "\nint saved = 1;\n" diff --git a/tests/integration/test_lifecycle.py b/tests/integration/test_lifecycle.py index bb292da9..f167721d 100644 --- a/tests/integration/test_lifecycle.py +++ b/tests/integration/test_lifecycle.py @@ -1,38 +1,27 @@ -"""Lifecycle tests for the clice LSP server using pygls.""" +"""Lifecycle tests for the clice LSP server.""" import pytest -from lsprotocol.types import ( - ClientCapabilities, - InitializeParams, - InitializedParams, - WorkspaceFolder, -) +from lsprotocol.types import ClientCapabilities, InitializeParams -@pytest.mark.asyncio -async def test_initialize(client, test_data_dir): - ws = test_data_dir / "hello_world" - result = await client.initialize_async( - InitializeParams( - capabilities=ClientCapabilities(), - root_uri=ws.as_uri(), - workspace_folders=[WorkspaceFolder(uri=ws.as_uri(), name="test")], +@pytest.mark.workspace("hello_world") +async def test_initialize(client, workspace): + assert client.init_result is not None + assert client.init_result.server_info is not None + assert client.init_result.server_info.name == "clice" + + +@pytest.mark.workspace("hello_world") +async def test_double_initialize_rejected(client, workspace): + with pytest.raises(Exception): + await client.initialize_async( + InitializeParams( + capabilities=ClientCapabilities(), + workspace_folders=[], + ) ) - ) - client.initialized(InitializedParams()) - assert result.server_info is not None - assert result.server_info.name == "clice" -@pytest.mark.asyncio -async def test_shutdown(client, test_data_dir): - ws = test_data_dir / "hello_world" - await client.initialize_async( - InitializeParams( - capabilities=ClientCapabilities(), - root_uri=ws.as_uri(), - workspace_folders=[WorkspaceFolder(uri=ws.as_uri(), name="test")], - ) - ) - client.initialized(InitializedParams()) +@pytest.mark.workspace("hello_world") +async def test_shutdown(client, workspace): await client.shutdown_async(None) diff --git a/tests/integration/test_modules.py b/tests/integration/test_modules.py index 6f043ad2..3f7ea22a 100644 --- a/tests/integration/test_modules.py +++ b/tests/integration/test_modules.py @@ -1,442 +1,172 @@ -"""Integration tests for C++20 module support through the full LSP server. - -These are the Python equivalents of the C++ compile_graph_integration_tests -and module_worker_tests. They test the complete pipeline: - MasterServer -> CompileGraph -> WorkerPool -> stateless/stateful workers. -""" +"""Integration tests for C++20 module support.""" import asyncio -import json import shutil -from pathlib import Path import pytest +from tests.conftest import generate_cdb from lsprotocol.types import ( - ClientCapabilities, DidCloseTextDocumentParams, DidOpenTextDocumentParams, HoverParams, - InitializeParams, - InitializedParams, Position, TextDocumentIdentifier, TextDocumentItem, - WorkspaceFolder, ) -# Directory containing pre-written module source files for each test case. -_DATA_DIR = Path(__file__).resolve().parent.parent / "data" / "modules" - -# --------------------------------------------------------------------------- -# Helpers -# --------------------------------------------------------------------------- - - -def _write_cdb(workspace: Path, files: list[str], extra_args: list[str] | None = None): - """Generate compile_commands.json for the given source files.""" - _write_cdb_entries(workspace, [(f, extra_args or []) for f in files]) - - -def _write_cdb_entries(workspace: Path, entries: list[tuple[str, list[str]]]): - """Generate compile_commands.json with per-file extra args. - - entries: list of (filename, extra_args) tuples. - """ - cdb = [] - for filename, extra in entries: - args = ["clang++", "-std=c++20", "-fsyntax-only"] - args.extend(extra) - args.append((workspace / filename).as_posix()) - cdb.append( - { - "directory": workspace.as_posix(), - "file": (workspace / filename).as_posix(), - "arguments": args, - } - ) - (workspace / "compile_commands.json").write_text(json.dumps(cdb, indent=2)) - - -async def _init(client, workspace: Path): - """Initialize the LSP server with a workspace.""" - result = await client.initialize_async( - InitializeParams( - capabilities=ClientCapabilities(), - root_uri=workspace.as_uri(), - workspace_folders=[WorkspaceFolder(uri=workspace.as_uri(), name="test")], - ) - ) - client.initialized(InitializedParams()) - # Give the server time to load CDB and scan dependency graph. - # Use a generous sleep to avoid flaky failures on slow CI machines. - await asyncio.sleep(2.0) - return result - - -def _open(client, workspace: Path, filename: str, version: int = 0): - """Open a file and return its URI.""" - path = workspace / filename - content = path.read_text(encoding="utf-8") - uri = path.as_uri() - client.text_document_did_open( - DidOpenTextDocumentParams( - text_document=TextDocumentItem( - uri=uri, language_id="cpp", version=version, text=content - ) - ) - ) - return uri, content - - -async def _open_and_wait(client, workspace: Path, filename: str, timeout: float = 60.0): - """Open a file and wait for compilation diagnostics.""" - uri, content = _open(client, workspace, filename) - event = client.wait_for_diagnostics(uri) - await asyncio.wait_for(event.wait(), timeout=timeout) - return uri, content - - -# --------------------------------------------------------------------------- -# Single module (no dependencies) -# --------------------------------------------------------------------------- - - -@pytest.mark.asyncio -async def test_single_module_no_deps(client): - """A single module with no imports should compile without errors.""" - ws = _DATA_DIR / "single_module_no_deps" - _write_cdb(ws, ["mod_a.cppm"]) - await _init(client, ws) - - uri, _ = await _open_and_wait(client, ws, "mod_a.cppm") +@pytest.mark.workspace("modules/single_module_no_deps") +async def test_single_module_no_deps(client, workspace): + uri, _ = await client.open_and_wait(workspace / "mod_a.cppm") diags = client.diagnostics.get(uri, []) assert len(diags) == 0, f"Expected no diagnostics, got: {diags}" -# --------------------------------------------------------------------------- -# Chained modules (A -> B, open B) -# --------------------------------------------------------------------------- - - -@pytest.mark.asyncio -async def test_chained_modules(client): - """Opening a module that imports another should trigger dependency compilation.""" - ws = _DATA_DIR / "chained_modules" - _write_cdb(ws, ["mod_a.cppm", "mod_b.cppm"]) - await _init(client, ws) - - uri, _ = await _open_and_wait(client, ws, "mod_b.cppm") +@pytest.mark.workspace("modules/chained_modules") +async def test_chained_modules(client, workspace): + """Opening mod_b that imports mod_a should trigger dependency compilation.""" + uri, _ = await client.open_and_wait(workspace / "mod_b.cppm") diags = client.diagnostics.get(uri, []) assert len(diags) == 0, f"Expected no diagnostics, got: {diags}" -# --------------------------------------------------------------------------- -# Diamond dependency (Base -> Left/Right -> Top) -# --------------------------------------------------------------------------- - - -@pytest.mark.asyncio -async def test_diamond_modules(client): - """Diamond dependency graph should compile correctly.""" - ws = _DATA_DIR / "diamond_modules" - _write_cdb(ws, ["base.cppm", "left.cppm", "right.cppm", "top.cppm"]) - await _init(client, ws) - - uri, _ = await _open_and_wait(client, ws, "top.cppm") +@pytest.mark.workspace("modules/diamond_modules") +async def test_diamond_modules(client, workspace): + uri, _ = await client.open_and_wait(workspace / "top.cppm") diags = client.diagnostics.get(uri, []) assert len(diags) == 0, f"Expected no diagnostics, got: {diags}" -# --------------------------------------------------------------------------- -# Dotted module name (my.io, my.app) -# --------------------------------------------------------------------------- - - -@pytest.mark.asyncio -async def test_dotted_module_name(client): - """Dotted module names should work correctly.""" - ws = _DATA_DIR / "dotted_module_name" - _write_cdb(ws, ["io.cppm", "app.cppm"]) - await _init(client, ws) - - uri, _ = await _open_and_wait(client, ws, "app.cppm") +@pytest.mark.workspace("modules/dotted_module_name") +async def test_dotted_module_name(client, workspace): + uri, _ = await client.open_and_wait(workspace / "app.cppm") diags = client.diagnostics.get(uri, []) assert len(diags) == 0, f"Expected no diagnostics, got: {diags}" -# --------------------------------------------------------------------------- -# Module implementation unit (module M; without export) -# --------------------------------------------------------------------------- - - -@pytest.mark.asyncio -async def test_module_implementation_unit(client): - """A module implementation unit should compile using the interface PCM.""" - ws = _DATA_DIR / "module_implementation_unit" - _write_cdb(ws, ["greeter.cppm", "greeter_impl.cpp"]) - await _init(client, ws) - - uri, _ = await _open_and_wait(client, ws, "greeter_impl.cpp") +@pytest.mark.workspace("modules/module_implementation_unit") +async def test_module_implementation_unit(client, workspace): + """Implementation unit (module M; without export) should compile using the interface PCM.""" + uri, _ = await client.open_and_wait(workspace / "greeter_impl.cpp") diags = client.diagnostics.get(uri, []) assert len(diags) == 0, f"Expected no diagnostics, got: {diags}" -# --------------------------------------------------------------------------- -# Consumer file that imports a module (regular .cpp) -# --------------------------------------------------------------------------- - - -@pytest.mark.asyncio -async def test_consumer_imports_module(client): - """A regular .cpp file that imports a module should get PCM deps compiled.""" - ws = _DATA_DIR / "consumer_imports_module" - _write_cdb(ws, ["math.cppm", "main.cpp"]) - await _init(client, ws) - - uri, _ = await _open_and_wait(client, ws, "main.cpp") +@pytest.mark.workspace("modules/consumer_imports_module") +async def test_consumer_imports_module(client, workspace): + """A regular .cpp that imports a module should get PCM deps compiled first.""" + uri, _ = await client.open_and_wait(workspace / "main.cpp") diags = client.diagnostics.get(uri, []) assert len(diags) == 0, f"Expected no diagnostics, got: {diags}" -# --------------------------------------------------------------------------- -# Module partitions (multiple partitions) -# --------------------------------------------------------------------------- - - -@pytest.mark.asyncio -async def test_module_partitions(client): - """Module partitions should be compiled in correct order.""" - ws = _DATA_DIR / "module_partitions" - _write_cdb(ws, ["part_a.cppm", "part_b.cppm", "lib.cppm"]) - await _init(client, ws) - - uri, _ = await _open_and_wait(client, ws, "lib.cppm") +@pytest.mark.workspace("modules/module_partitions") +async def test_module_partitions(client, workspace): + """Partitions should be compiled in correct dependency order.""" + uri, _ = await client.open_and_wait(workspace / "lib.cppm") diags = client.diagnostics.get(uri, []) assert len(diags) == 0, f"Expected no diagnostics, got: {diags}" -# --------------------------------------------------------------------------- -# Partition interface (single partition) -# --------------------------------------------------------------------------- - - -@pytest.mark.asyncio -async def test_partition_interface(client): - """A single partition interface re-exported from primary should compile.""" - ws = _DATA_DIR / "partition_interface" - _write_cdb(ws, ["part.cppm", "primary.cppm"]) - await _init(client, ws) - - uri, _ = await _open_and_wait(client, ws, "primary.cppm") +@pytest.mark.workspace("modules/partition_interface") +async def test_partition_interface(client, workspace): + uri, _ = await client.open_and_wait(workspace / "primary.cppm") diags = client.diagnostics.get(uri, []) assert len(diags) == 0, f"Expected no diagnostics, got: {diags}" -# --------------------------------------------------------------------------- -# Partition chain (partition importing another partition) -# --------------------------------------------------------------------------- - - -@pytest.mark.asyncio -async def test_partition_chain(client): - """Partition importing another partition within same module.""" - ws = _DATA_DIR / "partition_chain" - _write_cdb(ws, ["types.cppm", "core.cppm", "sys.cppm"]) - await _init(client, ws) - - uri, _ = await _open_and_wait(client, ws, "sys.cppm") +@pytest.mark.workspace("modules/partition_chain") +async def test_partition_chain(client, workspace): + uri, _ = await client.open_and_wait(workspace / "sys.cppm") diags = client.diagnostics.get(uri, []) assert len(diags) == 0, f"Expected no diagnostics, got: {diags}" -# --------------------------------------------------------------------------- -# Re-export (export import) -# --------------------------------------------------------------------------- - - -@pytest.mark.asyncio -async def test_re_export(client): - """Re-exported module symbols should be accessible through the wrapper.""" - ws = _DATA_DIR / "re_export" - _write_cdb(ws, ["core.cppm", "wrapper.cppm", "user.cppm"]) - await _init(client, ws) - - uri, _ = await _open_and_wait(client, ws, "user.cppm") +@pytest.mark.workspace("modules/re_export") +async def test_re_export(client, workspace): + """Re-exported symbols (export import) should be accessible through the wrapper.""" + uri, _ = await client.open_and_wait(workspace / "user.cppm") diags = client.diagnostics.get(uri, []) assert len(diags) == 0, f"Expected no diagnostics, got: {diags}" -# --------------------------------------------------------------------------- -# Export block syntax -# --------------------------------------------------------------------------- - - -@pytest.mark.asyncio -async def test_export_block(client): - """Module with export block syntax should compile correctly.""" - ws = _DATA_DIR / "export_block" - _write_cdb(ws, ["block.cppm", "consumer.cppm"]) - await _init(client, ws) - - uri, _ = await _open_and_wait(client, ws, "consumer.cppm") +@pytest.mark.workspace("modules/export_block") +async def test_export_block(client, workspace): + uri, _ = await client.open_and_wait(workspace / "consumer.cppm") diags = client.diagnostics.get(uri, []) assert len(diags) == 0, f"Expected no diagnostics, got: {diags}" -# --------------------------------------------------------------------------- -# Global module fragment -# --------------------------------------------------------------------------- - - -@pytest.mark.asyncio -async def test_global_module_fragment(client): - """Module with global module fragment (#include before module decl).""" - ws = _DATA_DIR / "global_module_fragment" - _write_cdb(ws, ["gmf.cppm"], extra_args=["-I", ws.as_posix()]) - await _init(client, ws) - - uri, _ = await _open_and_wait(client, ws, "gmf.cppm") +@pytest.mark.workspace("modules/global_module_fragment") +async def test_global_module_fragment(client, workspace): + uri, _ = await client.open_and_wait(workspace / "gmf.cppm") diags = client.diagnostics.get(uri, []) assert len(diags) == 0, f"Expected no diagnostics, got: {diags}" -# --------------------------------------------------------------------------- -# Private module fragment -# --------------------------------------------------------------------------- - - -@pytest.mark.asyncio -async def test_private_module_fragment(client): - """Module with private module fragment should compile correctly.""" - ws = _DATA_DIR / "private_module_fragment" - _write_cdb(ws, ["priv.cppm"]) - await _init(client, ws) - - uri, _ = await _open_and_wait(client, ws, "priv.cppm") +@pytest.mark.workspace("modules/private_module_fragment") +async def test_private_module_fragment(client, workspace): + uri, _ = await client.open_and_wait(workspace / "priv.cppm") diags = client.diagnostics.get(uri, []) assert len(diags) == 0, f"Expected no diagnostics, got: {diags}" -# --------------------------------------------------------------------------- -# Export namespace -# --------------------------------------------------------------------------- - - -@pytest.mark.asyncio -async def test_export_namespace(client): - """Module with exported namespace should compile correctly.""" - ws = _DATA_DIR / "export_namespace" - _write_cdb(ws, ["ns.cppm", "calc.cppm"]) - await _init(client, ws) - - uri, _ = await _open_and_wait(client, ws, "calc.cppm") +@pytest.mark.workspace("modules/export_namespace") +async def test_export_namespace(client, workspace): + uri, _ = await client.open_and_wait(workspace / "calc.cppm") diags = client.diagnostics.get(uri, []) assert len(diags) == 0, f"Expected no diagnostics, got: {diags}" -# --------------------------------------------------------------------------- -# GMF with include + module import -# --------------------------------------------------------------------------- - - -@pytest.mark.asyncio -async def test_gmf_with_import(client): - """Module with GMF (#include) + import should compile correctly.""" - ws = _DATA_DIR / "gmf_with_import" - _write_cdb_entries( - ws, - [ - ("base.cppm", []), - ("combined.cppm", ["-I", ws.as_posix()]), - ], - ) - await _init(client, ws) - - uri, _ = await _open_and_wait(client, ws, "combined.cppm") +@pytest.mark.workspace("modules/gmf_with_import") +async def test_gmf_with_import(client, workspace): + uri, _ = await client.open_and_wait(workspace / "combined.cppm") diags = client.diagnostics.get(uri, []) assert len(diags) == 0, f"Expected no diagnostics, got: {diags}" -# --------------------------------------------------------------------------- -# Independent modules (no shared deps) -# --------------------------------------------------------------------------- - - -@pytest.mark.asyncio -async def test_independent_modules(client): - """Two independent modules should each compile without errors.""" - ws = _DATA_DIR / "independent_modules" - _write_cdb(ws, ["x.cppm", "y.cppm"]) - await _init(client, ws) - - uri_x, _ = await _open_and_wait(client, ws, "x.cppm") +@pytest.mark.workspace("modules/independent_modules") +async def test_independent_modules(client, workspace): + uri_x, _ = await client.open_and_wait(workspace / "x.cppm") diags_x = client.diagnostics.get(uri_x, []) assert len(diags_x) == 0, f"Expected no diagnostics for X, got: {diags_x}" - uri_y, _ = await _open_and_wait(client, ws, "y.cppm") + uri_y, _ = await client.open_and_wait(workspace / "y.cppm") diags_y = client.diagnostics.get(uri_y, []) assert len(diags_y) == 0, f"Expected no diagnostics for Y, got: {diags_y}" -# --------------------------------------------------------------------------- -# Template export -# --------------------------------------------------------------------------- - - -@pytest.mark.asyncio -async def test_template_export(client): - """Module with exported templates should compile correctly.""" - ws = _DATA_DIR / "template_export" - _write_cdb(ws, ["tmpl.cppm", "use_tmpl.cppm"]) - await _init(client, ws) - - uri, _ = await _open_and_wait(client, ws, "use_tmpl.cppm") +@pytest.mark.workspace("modules/template_export") +async def test_template_export(client, workspace): + uri, _ = await client.open_and_wait(workspace / "use_tmpl.cppm") diags = client.diagnostics.get(uri, []) assert len(diags) == 0, f"Expected no diagnostics, got: {diags}" -# --------------------------------------------------------------------------- -# Class export and inheritance across modules -# --------------------------------------------------------------------------- - - -@pytest.mark.asyncio -async def test_class_export_and_inheritance(client): - """Exported class with cross-module inheritance should compile.""" - ws = _DATA_DIR / "class_export_and_inheritance" - _write_cdb(ws, ["shape.cppm", "circle.cppm"]) - await _init(client, ws) - - uri, _ = await _open_and_wait(client, ws, "circle.cppm") +@pytest.mark.workspace("modules/class_export_and_inheritance") +async def test_class_export_and_inheritance(client, workspace): + uri, _ = await client.open_and_wait(workspace / "circle.cppm") diags = client.diagnostics.get(uri, []) assert len(diags) == 0, f"Expected no diagnostics, got: {diags}" -# --------------------------------------------------------------------------- -# Save triggers recompilation (close/reopen with new content) -# --------------------------------------------------------------------------- - - -@pytest.mark.asyncio -async def test_save_recompile(client, tmp_path): +async def test_save_recompile(client, test_data_dir, tmp_path): """Closing and reopening a modified module file should recompile without errors.""" - # This test mutates source files at runtime, so copy data to tmp_path. - src = _DATA_DIR / "save_recompile" + src = test_data_dir / "modules" / "save_recompile" for f in src.iterdir(): if f.is_file(): shutil.copy2(f, tmp_path / f.name) - _write_cdb(tmp_path, ["leaf.cppm", "mid.cppm"]) - await _init(client, tmp_path) + generate_cdb(tmp_path) + await client.initialize(tmp_path) # Open and compile Mid (which triggers Leaf PCM build). - mid_uri, _ = await _open_and_wait(client, tmp_path, "mid.cppm") + mid_uri, _ = await client.open_and_wait(tmp_path / "mid.cppm") diags = client.diagnostics.get(mid_uri, []) assert len(diags) == 0 # Open Leaf and wait for its initial compilation. - leaf_uri, _ = _open(client, tmp_path, "leaf.cppm") + leaf_uri, _ = client.open(tmp_path / "leaf.cppm") event = client.wait_for_diagnostics(leaf_uri) await asyncio.wait_for(event.wait(), timeout=60.0) @@ -448,7 +178,6 @@ async def test_save_recompile(client, tmp_path): new_content = "export module Leaf;\nexport int leaf() { return 100; }\n" (tmp_path / "leaf.cppm").write_text(new_content) - # Reopen with new content triggers compilation. event = client.wait_for_diagnostics(leaf_uri) client.text_document_did_open( DidOpenTextDocumentParams( @@ -459,111 +188,49 @@ async def test_save_recompile(client, tmp_path): ) await asyncio.wait_for(event.wait(), timeout=60.0) - # Should still compile without errors after change. diags = client.diagnostics.get(leaf_uri, []) assert len(diags) == 0, f"Expected no diagnostics after save, got: {diags}" -# --------------------------------------------------------------------------- -# Compilation failure (undefined symbol in module) -# --------------------------------------------------------------------------- - - -@pytest.mark.asyncio -async def test_module_compile_error(client): - """A module with an error should produce diagnostics.""" - ws = _DATA_DIR / "module_compile_error" - _write_cdb(ws, ["good.cppm", "bad.cppm"]) - await _init(client, ws) - - uri, _ = await _open_and_wait(client, ws, "bad.cppm") +@pytest.mark.workspace("modules/module_compile_error") +async def test_module_compile_error(client, workspace): + uri, _ = await client.open_and_wait(workspace / "bad.cppm") diags = client.diagnostics.get(uri, []) assert len(diags) > 0, "Expected diagnostics for undefined symbol" - # The error should be on line 2 (0-indexed) where UNDEFINED_SYMBOL is used. - error_diag = diags[0] - assert error_diag.range.start.line == 2, ( - f"Expected error on line 2, got line {error_diag.range.start.line}" - ) - # Severity 1 = Error in LSP spec. - assert error_diag.severity == 1, ( - f"Expected severity Error (1), got {error_diag.severity}" + assert any(d.range.start.line == 4 and d.severity == 1 for d in diags), ( + f"Expected an error diagnostic on line 4, got: {diags}" ) -# --------------------------------------------------------------------------- -# Deep chain (5 modules) -# --------------------------------------------------------------------------- - - -@pytest.mark.asyncio -async def test_deep_chain(client): - """A 5-level module chain should compile correctly.""" - ws = _DATA_DIR / "deep_chain" - _write_cdb(ws, ["m1.cppm", "m2.cppm", "m3.cppm", "m4.cppm", "m5.cppm"]) - await _init(client, ws) - - uri, _ = await _open_and_wait(client, ws, "m5.cppm") +@pytest.mark.workspace("modules/deep_chain") +async def test_deep_chain(client, workspace): + """A 5-level module chain (m1->m2->...->m5) should compile correctly.""" + uri, _ = await client.open_and_wait(workspace / "m5.cppm") diags = client.diagnostics.get(uri, []) assert len(diags) == 0, f"Expected no diagnostics, got: {diags}" -# --------------------------------------------------------------------------- -# Partition with GMF (#include inside global module fragment of partition) -# --------------------------------------------------------------------------- - - -@pytest.mark.asyncio -async def test_partition_with_gmf(client): - """Partition with GMF (#include) should compile correctly.""" - ws = _DATA_DIR / "partition_with_gmf" - _write_cdb_entries( - ws, - [ - ("part_cfg.cppm", ["-I", ws.as_posix()]), - ("cfg.cppm", []), - ], - ) - await _init(client, ws) - - uri, _ = await _open_and_wait(client, ws, "cfg.cppm") +@pytest.mark.workspace("modules/partition_with_gmf") +async def test_partition_with_gmf(client, workspace): + uri, _ = await client.open_and_wait(workspace / "cfg.cppm") diags = client.diagnostics.get(uri, []) assert len(diags) == 0, f"Expected no diagnostics, got: {diags}" -# --------------------------------------------------------------------------- -# Cross-module partition + external import -# --------------------------------------------------------------------------- - - -@pytest.mark.asyncio -async def test_partition_with_external_import(client): - """Partition importing an external module should compile correctly.""" - ws = _DATA_DIR / "partition_with_external_import" - _write_cdb(ws, ["ext.cppm", "part.cppm", "app.cppm"]) - await _init(client, ws) - - uri, _ = await _open_and_wait(client, ws, "app.cppm") +@pytest.mark.workspace("modules/partition_with_external_import") +async def test_partition_with_external_import(client, workspace): + uri, _ = await client.open_and_wait(workspace / "app.cppm") diags = client.diagnostics.get(uri, []) assert len(diags) == 0, f"Expected no diagnostics, got: {diags}" -# --------------------------------------------------------------------------- -# Hover on imported symbol (feature request after module compilation) -# --------------------------------------------------------------------------- - - -@pytest.mark.asyncio -async def test_hover_on_imported_symbol(client): - """Hover on a symbol imported from a module should return info.""" - ws = _DATA_DIR / "hover_on_imported_symbol" - _write_cdb(ws, ["defs.cppm", "use.cpp"]) - await _init(client, ws) - - uri, _ = await _open_and_wait(client, ws, "use.cpp") +@pytest.mark.workspace("modules/hover_on_imported_symbol") +async def test_hover_on_imported_symbol(client, workspace): + """Hover on a symbol imported from a module should return type info.""" + uri, _ = await client.open_and_wait(workspace / "use.cpp") diags = client.diagnostics.get(uri, []) assert len(diags) == 0, f"Expected no diagnostics, got: {diags}" - # Hover on 'magic_number' (line 3, character 11 = start of 'magic_number()') hover = await client.text_document_hover_async( HoverParams( text_document=TextDocumentIdentifier(uri=uri), @@ -574,50 +241,25 @@ async def test_hover_on_imported_symbol(client): assert hover.contents is not None -# --------------------------------------------------------------------------- -# Plain C++ file with no modules (compile_graph == null path) -# --------------------------------------------------------------------------- - - -@pytest.mark.asyncio -async def test_no_modules_plain_cpp(client): - """A plain C++ file with no modules should compile normally (no CompileGraph).""" - ws = _DATA_DIR / "no_modules_plain_cpp" - _write_cdb(ws, ["plain.cpp"]) - await _init(client, ws) - - uri, _ = await _open_and_wait(client, ws, "plain.cpp") +@pytest.mark.workspace("modules/no_modules_plain_cpp") +async def test_no_modules_plain_cpp(client, workspace): + """Plain .cpp with no modules should compile normally (CompileGraph null path).""" + uri, _ = await client.open_and_wait(workspace / "plain.cpp") diags = client.diagnostics.get(uri, []) assert len(diags) == 0, f"Expected no diagnostics, got: {diags}" -# --------------------------------------------------------------------------- -# Circular module dependency (cycle detection) -# --------------------------------------------------------------------------- - - -@pytest.mark.asyncio -async def test_circular_module_dependency(client): +@pytest.mark.workspace("modules/circular_module_dependency") +async def test_circular_module_dependency(client, workspace): """Circular module imports should not hang the server. - When modules form a cycle (CycA imports CycB, CycB imports CycA), - the CompileGraph's cycle detection should prevent deadlock. The PCM - builds will fail, so the server may skip the final compilation and - never publish diagnostics. The key assertion is that the server - remains responsive — we verify this by successfully performing a - subsequent operation (opening a non-cyclic file). + The CompileGraph's cycle detection should prevent deadlock. We verify + the server remains responsive by opening a non-cyclic file afterwards. """ - ws = _DATA_DIR / "circular_module_dependency" - _write_cdb(ws, ["cycle_a.cppm", "cycle_b.cppm", "ok.cppm"]) - await _init(client, ws) - - # Open a cyclic file — the server should not hang. - _open(client, ws, "cycle_a.cppm") - # Give the server time to attempt (and fail) the cyclic PCM builds. + client.open(workspace / "cycle_a.cppm") await asyncio.sleep(5.0) - # Verify the server is still responsive by opening a non-cyclic file. - uri_ok, _ = await _open_and_wait(client, ws, "ok.cppm") + uri_ok, _ = await client.open_and_wait(workspace / "ok.cppm") diags = client.diagnostics.get(uri_ok, []) assert len(diags) == 0, ( f"Non-cyclic module should compile fine after cycle attempt, got: {diags}" diff --git a/tests/integration/test_server.py b/tests/integration/test_server.py index ccd5903d..adaa67f3 100644 --- a/tests/integration/test_server.py +++ b/tests/integration/test_server.py @@ -1,25 +1,20 @@ """Integration tests for the clice MasterServer using pygls.""" import asyncio -from pathlib import Path import pytest from lsprotocol.types import ( - ClientCapabilities, CodeActionContext, CodeActionParams, CompletionParams, DefinitionParams, DidCloseTextDocumentParams, - DidOpenTextDocumentParams, DidChangeTextDocumentParams, DidSaveTextDocumentParams, DocumentLinkParams, DocumentSymbolParams, FoldingRangeParams, HoverParams, - InitializeParams, - InitializedParams, InlayHintParams, Position, Range, @@ -27,100 +22,23 @@ from lsprotocol.types import ( SignatureHelpParams, TextDocumentContentChangeWholeDocument, TextDocumentIdentifier, - TextDocumentItem, VersionedTextDocumentIdentifier, - WorkspaceFolder, ) -# --------------------------------------------------------------------------- -# Helpers -# --------------------------------------------------------------------------- - - -def _workspace_uri(test_data_dir: Path, name: str = "hello_world") -> str: - return (test_data_dir / name).as_uri() - - -def _file_uri( - test_data_dir: Path, name: str = "hello_world", file: str = "main.cpp" -) -> str: - return (test_data_dir / name / file).as_uri() - - def _doc(uri: str) -> TextDocumentIdentifier: return TextDocumentIdentifier(uri=uri) -async def _initialize(client, test_data_dir: Path, name: str = "hello_world"): - """Initialize with a workspace folder.""" - ws = test_data_dir / name - result = await client.initialize_async( - InitializeParams( - capabilities=ClientCapabilities(), - root_uri=ws.as_uri(), - workspace_folders=[WorkspaceFolder(uri=ws.as_uri(), name="test")], - ) - ) - client.initialized(InitializedParams()) - return result +@pytest.mark.workspace("hello_world") +async def test_server_info(client, workspace): + assert client.init_result.server_info.name == "clice" + assert client.init_result.server_info.version == "0.1.0" -async def _open_file( - client, test_data_dir: Path, name: str = "hello_world", file: str = "main.cpp" -): - """Open a text document.""" - path = test_data_dir / name / file - content = path.read_text(encoding="utf-8") - uri = path.as_uri() - client.text_document_did_open( - DidOpenTextDocumentParams( - text_document=TextDocumentItem( - uri=uri, - language_id="cpp", - version=0, - text=content, - ) - ) - ) - return uri, content - - -async def _wait_for_compilation(client, uri: str, timeout: float = 30.0): - """Wait for diagnostics on the given URI.""" - event = client.wait_for_diagnostics(uri) - await asyncio.wait_for(event.wait(), timeout=timeout) - - -async def _open_and_wait( - client, - test_data_dir: Path, - name: str = "hello_world", - file: str = "main.cpp", - timeout: float = 30.0, -): - """Open file and wait for compilation diagnostics.""" - uri, content = await _open_file(client, test_data_dir, name, file) - await _wait_for_compilation(client, uri, timeout) - return uri, content - - -# --------------------------------------------------------------------------- -# Server info & capabilities -# --------------------------------------------------------------------------- - - -@pytest.mark.asyncio -async def test_server_info(client, test_data_dir): - result = await _initialize(client, test_data_dir) - assert result.server_info.name == "clice" - assert result.server_info.version == "0.1.0" - - -@pytest.mark.asyncio -async def test_capabilities(client, test_data_dir): - result = await _initialize(client, test_data_dir) - caps = result.capabilities +@pytest.mark.workspace("hello_world") +async def test_capabilities(client, workspace): + caps = client.init_result.capabilities assert caps.hover_provider is True assert caps.completion_provider is not None assert caps.definition_provider is True @@ -131,41 +49,21 @@ async def test_capabilities(client, test_data_dir): assert caps.semantic_tokens_provider is not None -# --------------------------------------------------------------------------- -# Initialization & shutdown -# --------------------------------------------------------------------------- - - -@pytest.mark.asyncio -async def test_double_initialize_rejected(client, test_data_dir): - await _initialize(client, test_data_dir) - with pytest.raises(Exception): - await client.initialize_async( - InitializeParams( - capabilities=ClientCapabilities(), - workspace_folders=[], - ) - ) - - -@pytest.mark.asyncio -async def test_did_open_close_cycle(client, test_data_dir): - await _initialize(client, test_data_dir) - uri, _ = await _open_file(client, test_data_dir) +@pytest.mark.workspace("hello_world") +async def test_did_open_close_cycle(client, workspace): + uri, _ = client.open(workspace / "main.cpp") await asyncio.sleep(0.5) client.text_document_did_close(DidCloseTextDocumentParams(text_document=_doc(uri))) -@pytest.mark.asyncio -async def test_shutdown_exit(client, test_data_dir): - await _initialize(client, test_data_dir) +@pytest.mark.workspace("hello_world") +async def test_shutdown_exit(client, workspace): await client.shutdown_async(None) -@pytest.mark.asyncio -async def test_feature_requests_after_close(client, test_data_dir): - await _initialize(client, test_data_dir) - uri, _ = await _open_file(client, test_data_dir) +@pytest.mark.workspace("hello_world") +async def test_feature_requests_after_close(client, workspace): + uri, _ = client.open(workspace / "main.cpp") client.text_document_did_close(DidCloseTextDocumentParams(text_document=_doc(uri))) result = await client.text_document_hover_async( HoverParams(text_document=_doc(uri), position=Position(line=0, character=0)) @@ -173,15 +71,9 @@ async def test_feature_requests_after_close(client, test_data_dir): assert result is None -# --------------------------------------------------------------------------- -# Document handling -# --------------------------------------------------------------------------- - - -@pytest.mark.asyncio -async def test_incremental_change(client, test_data_dir): - await _initialize(client, test_data_dir) - uri, content = await _open_file(client, test_data_dir) +@pytest.mark.workspace("hello_world") +async def test_incremental_change(client, workspace): + uri, content = client.open(workspace / "main.cpp") for i in range(5): content += f"\n// change {i}" client.text_document_did_change( @@ -195,34 +87,25 @@ async def test_incremental_change(client, test_data_dir): client.text_document_did_close(DidCloseTextDocumentParams(text_document=_doc(uri))) -@pytest.mark.asyncio -async def test_diagnostics_received(client, test_data_dir): - await _initialize(client, test_data_dir) - uri, _ = await _open_and_wait(client, test_data_dir) +@pytest.mark.workspace("hello_world") +async def test_diagnostics_received(client, workspace): + uri, _ = await client.open_and_wait(workspace / "main.cpp") assert uri in client.diagnostics client.text_document_did_close(DidCloseTextDocumentParams(text_document=_doc(uri))) -# --------------------------------------------------------------------------- -# Feature requests (after compilation) -# --------------------------------------------------------------------------- - - -@pytest.mark.asyncio -async def test_hover_before_compile(client, test_data_dir): - await _initialize(client, test_data_dir) - uri, _ = await _open_file(client, test_data_dir) +@pytest.mark.workspace("hello_world") +async def test_hover_before_compile(client, workspace): + uri, _ = client.open(workspace / "main.cpp") result = await client.text_document_hover_async( HoverParams(text_document=_doc(uri), position=Position(line=0, character=0)) ) - # May return null before compilation — that's fine client.text_document_did_close(DidCloseTextDocumentParams(text_document=_doc(uri))) -@pytest.mark.asyncio -async def test_completion_request(client, test_data_dir): - await _initialize(client, test_data_dir) - uri, _ = await _open_and_wait(client, test_data_dir) +@pytest.mark.workspace("hello_world") +async def test_completion_request(client, workspace): + uri, _ = await client.open_and_wait(workspace / "main.cpp") result = await client.text_document_completion_async( CompletionParams( text_document=_doc(uri), position=Position(line=0, character=0) @@ -231,10 +114,9 @@ async def test_completion_request(client, test_data_dir): client.text_document_did_close(DidCloseTextDocumentParams(text_document=_doc(uri))) -@pytest.mark.asyncio -async def test_signature_help_request(client, test_data_dir): - await _initialize(client, test_data_dir) - uri, _ = await _open_and_wait(client, test_data_dir) +@pytest.mark.workspace("hello_world") +async def test_signature_help_request(client, workspace): + uri, _ = await client.open_and_wait(workspace / "main.cpp") result = await client.text_document_signature_help_async( SignatureHelpParams( text_document=_doc(uri), position=Position(line=0, character=0) @@ -243,10 +125,9 @@ async def test_signature_help_request(client, test_data_dir): client.text_document_did_close(DidCloseTextDocumentParams(text_document=_doc(uri))) -@pytest.mark.asyncio -async def test_definition_request(client, test_data_dir): - await _initialize(client, test_data_dir) - uri, _ = await _open_and_wait(client, test_data_dir) +@pytest.mark.workspace("hello_world") +async def test_definition_request(client, workspace): + uri, _ = await client.open_and_wait(workspace / "main.cpp") result = await client.text_document_definition_async( DefinitionParams( text_document=_doc(uri), position=Position(line=0, character=4) @@ -255,10 +136,9 @@ async def test_definition_request(client, test_data_dir): client.text_document_did_close(DidCloseTextDocumentParams(text_document=_doc(uri))) -@pytest.mark.asyncio -async def test_document_symbol_request(client, test_data_dir): - await _initialize(client, test_data_dir) - uri, _ = await _open_and_wait(client, test_data_dir) +@pytest.mark.workspace("hello_world") +async def test_document_symbol_request(client, workspace): + uri, _ = await client.open_and_wait(workspace / "main.cpp") result = await client.text_document_document_symbol_async( DocumentSymbolParams(text_document=_doc(uri)) ) @@ -266,10 +146,9 @@ async def test_document_symbol_request(client, test_data_dir): client.text_document_did_close(DidCloseTextDocumentParams(text_document=_doc(uri))) -@pytest.mark.asyncio -async def test_folding_range_request(client, test_data_dir): - await _initialize(client, test_data_dir) - uri, _ = await _open_and_wait(client, test_data_dir) +@pytest.mark.workspace("hello_world") +async def test_folding_range_request(client, workspace): + uri, _ = await client.open_and_wait(workspace / "main.cpp") result = await client.text_document_folding_range_async( FoldingRangeParams(text_document=_doc(uri)) ) @@ -277,10 +156,9 @@ async def test_folding_range_request(client, test_data_dir): client.text_document_did_close(DidCloseTextDocumentParams(text_document=_doc(uri))) -@pytest.mark.asyncio -async def test_semantic_tokens_request(client, test_data_dir): - await _initialize(client, test_data_dir) - uri, _ = await _open_and_wait(client, test_data_dir) +@pytest.mark.workspace("hello_world") +async def test_semantic_tokens_request(client, workspace): + uri, _ = await client.open_and_wait(workspace / "main.cpp") result = await client.text_document_semantic_tokens_full_async( SemanticTokensParams(text_document=_doc(uri)) ) @@ -288,10 +166,9 @@ async def test_semantic_tokens_request(client, test_data_dir): client.text_document_did_close(DidCloseTextDocumentParams(text_document=_doc(uri))) -@pytest.mark.asyncio -async def test_inlay_hint_request(client, test_data_dir): - await _initialize(client, test_data_dir) - uri, _ = await _open_and_wait(client, test_data_dir) +@pytest.mark.workspace("hello_world") +async def test_inlay_hint_request(client, workspace): + uri, _ = await client.open_and_wait(workspace / "main.cpp") result = await client.text_document_inlay_hint_async( InlayHintParams( text_document=_doc(uri), @@ -303,10 +180,9 @@ async def test_inlay_hint_request(client, test_data_dir): client.text_document_did_close(DidCloseTextDocumentParams(text_document=_doc(uri))) -@pytest.mark.asyncio -async def test_code_action_request(client, test_data_dir): - await _initialize(client, test_data_dir) - uri, _ = await _open_and_wait(client, test_data_dir) +@pytest.mark.workspace("hello_world") +async def test_code_action_request(client, workspace): + uri, _ = await client.open_and_wait(workspace / "main.cpp") result = await client.text_document_code_action_async( CodeActionParams( text_document=_doc(uri), @@ -319,10 +195,9 @@ async def test_code_action_request(client, test_data_dir): client.text_document_did_close(DidCloseTextDocumentParams(text_document=_doc(uri))) -@pytest.mark.asyncio -async def test_document_link_request(client, test_data_dir): - await _initialize(client, test_data_dir) - uri, _ = await _open_and_wait(client, test_data_dir) +@pytest.mark.workspace("hello_world") +async def test_document_link_request(client, workspace): + uri, _ = await client.open_and_wait(workspace / "main.cpp") result = await client.text_document_document_link_async( DocumentLinkParams(text_document=_doc(uri)) ) @@ -330,15 +205,9 @@ async def test_document_link_request(client, test_data_dir): client.text_document_did_close(DidCloseTextDocumentParams(text_document=_doc(uri))) -# --------------------------------------------------------------------------- -# Stress and edge cases -# --------------------------------------------------------------------------- - - -@pytest.mark.asyncio -async def test_rapid_changes_stress(client, test_data_dir): - await _initialize(client, test_data_dir) - uri, content = await _open_file(client, test_data_dir) +@pytest.mark.workspace("hello_world") +async def test_rapid_changes_stress(client, workspace): + uri, content = client.open(workspace / "main.cpp") for i in range(20): content += f"\n// stress change {i}\n" client.text_document_did_change( @@ -351,19 +220,17 @@ async def test_rapid_changes_stress(client, test_data_dir): client.text_document_did_close(DidCloseTextDocumentParams(text_document=_doc(uri))) -@pytest.mark.asyncio -async def test_save_notification(client, test_data_dir): - await _initialize(client, test_data_dir) - uri, _ = await _open_file(client, test_data_dir) +@pytest.mark.workspace("hello_world") +async def test_save_notification(client, workspace): + uri, _ = client.open(workspace / "main.cpp") await asyncio.sleep(0.5) client.text_document_did_save(DidSaveTextDocumentParams(text_document=_doc(uri))) await asyncio.sleep(0.5) client.text_document_did_close(DidCloseTextDocumentParams(text_document=_doc(uri))) -@pytest.mark.asyncio -async def test_hover_on_unknown_file(client, test_data_dir): - await _initialize(client, test_data_dir) +@pytest.mark.workspace("hello_world") +async def test_hover_on_unknown_file(client, workspace): result = await client.text_document_hover_async( HoverParams( text_document=_doc("file:///nonexistent/fake.cpp"), @@ -373,64 +240,54 @@ async def test_hover_on_unknown_file(client, test_data_dir): assert result is None -@pytest.mark.asyncio -async def test_all_features_after_compile_wait(client, test_data_dir): - """After waiting for compilation, exercise all feature requests.""" - await _initialize(client, test_data_dir) - uri, _ = await _open_and_wait(client, test_data_dir) +@pytest.mark.workspace("hello_world") +async def test_all_features_after_compile_wait(client, workspace): + """Exercise all feature requests after compilation completes.""" + uri, _ = await client.open_and_wait(workspace / "main.cpp") - # Hover on 'add' (line 0, character 4) hover = await client.text_document_hover_async( HoverParams(text_document=_doc(uri), position=Position(line=0, character=4)) ) assert hover is not None - # Completion completion = await client.text_document_completion_async( CompletionParams( text_document=_doc(uri), position=Position(line=5, character=18) ) ) - # Signature help await client.text_document_signature_help_async( SignatureHelpParams( text_document=_doc(uri), position=Position(line=0, character=0) ) ) - # Definition on 'add' await client.text_document_definition_async( DefinitionParams( text_document=_doc(uri), position=Position(line=0, character=4) ) ) - # Document symbols symbols = await client.text_document_document_symbol_async( DocumentSymbolParams(text_document=_doc(uri)) ) assert symbols is not None - # Folding ranges folding = await client.text_document_folding_range_async( FoldingRangeParams(text_document=_doc(uri)) ) assert folding is not None - # Semantic tokens tokens = await client.text_document_semantic_tokens_full_async( SemanticTokensParams(text_document=_doc(uri)) ) assert tokens is not None - # Document links links = await client.text_document_document_link_async( DocumentLinkParams(text_document=_doc(uri)) ) assert links is not None - # Code actions await client.text_document_code_action_async( CodeActionParams( text_document=_doc(uri), @@ -441,7 +298,6 @@ async def test_all_features_after_compile_wait(client, test_data_dir): ) ) - # Inlay hints await client.text_document_inlay_hint_async( InlayHintParams( text_document=_doc(uri), diff --git a/tests/pyproject.toml b/tests/pyproject.toml deleted file mode 100644 index 0b269e24..00000000 --- a/tests/pyproject.toml +++ /dev/null @@ -1,14 +0,0 @@ -[project] -name = "clice" -version = "0.1.0" -description = "clice is a language server for C/C++" -readme = "README.md" -requires-python = ">=3.13" -authors = [{ name = "clice developers" }] -dependencies = [] - -[dependency-groups] -dev = ["pre-commit>=4.3.0", "pytest", "pytest-asyncio>=1.1.0"] - -[tool.pytest.ini_options] -markers = ["asyncio"] diff --git a/tests/pytest.ini b/tests/pytest.ini new file mode 100644 index 00000000..50cc7b46 --- /dev/null +++ b/tests/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +asyncio_mode = auto +markers = workspace diff --git a/tests/unit/server/compile_graph_integration_tests.cpp b/tests/unit/server/compile_graph_integration_tests.cpp index 7b9b43c9..801d7ca2 100644 --- a/tests/unit/server/compile_graph_integration_tests.cpp +++ b/tests/unit/server/compile_graph_integration_tests.cpp @@ -13,11 +13,6 @@ namespace { namespace et = eventide; -void write_cdb(TempDir& tmp, CompilationDatabase& cdb, llvm::StringRef json_content) { - tmp.touch("compile_commands.json", json_content); - cdb.load(tmp.path("compile_commands.json")); -} - /// Build a dispatch_fn that compiles PCMs in-process (no workers). /// Clang requires ALL transitive PCM deps (not just direct imports) /// in PrebuiltModuleFiles, so we pass every available PCM. diff --git a/tests/unit/server/module_worker_tests.cpp b/tests/unit/server/module_worker_tests.cpp index afa5ae99..05b225c9 100644 --- a/tests/unit/server/module_worker_tests.cpp +++ b/tests/unit/server/module_worker_tests.cpp @@ -21,13 +21,15 @@ namespace et = eventide; TEST_SUITE(ModuleWorker) { TEST_CASE(BuildPCMThenCompileWithImport) { + TempDir tmp; // Module interface: produces PCM. - TempFile iface( - "mod_iface.cppm", - "export module Hello;\n" R"(export const char* hello() { return "world"; })" "\n"); + tmp.touch("mod_iface.cppm", + "export module Hello;\n" R"(export const char* hello() { return "world"; })" "\n"); + auto iface = tmp.path("mod_iface.cppm"); // Consumer: imports the module. - TempFile consumer("consumer.cpp", "import Hello;\n" "int main() { return hello()[0]; }\n"); + tmp.touch("consumer.cpp", "import Hello;\n" "int main() { return hello()[0]; }\n"); + auto consumer = tmp.path("consumer.cpp"); // --- Phase 1: Build PCM via stateless worker --- WorkerHandle sl; @@ -38,14 +40,14 @@ TEST_CASE(BuildPCMThenCompileWithImport) { sl.run([&]() -> et::task<> { worker::BuildPCMParams params; - params.file = iface.path; + params.file = iface; params.directory = "/tmp"; params.arguments = {"clang++", "-resource-dir", std::string(resource_dir()), "-std=c++20", "--precompile", - iface.path}; + iface}; params.module_name = "Hello"; auto result = co_await sl.peer->send_request(params); @@ -69,7 +71,7 @@ TEST_CASE(BuildPCMThenCompileWithImport) { sf.run([&]() -> et::task<> { worker::CompileParams params; - params.path = consumer.path; + params.path = consumer; params.version = 1; params.text = "import Hello;\n" "int main() { return hello()[0]; }\n"; params.directory = "/tmp"; @@ -78,7 +80,7 @@ TEST_CASE(BuildPCMThenCompileWithImport) { std::string(resource_dir()), "-std=c++20", "-fsyntax-only", - consumer.path}; + consumer}; // Pass the PCM — same as MasterServer fills CompileParams.pcms. params.pcms = { {"Hello", pcm_path} @@ -99,15 +101,19 @@ TEST_CASE(BuildPCMThenCompileWithImport) { } TEST_CASE(BuildPCMChainThenCompile) { + TempDir tmp; // Module A: no deps. - TempFile mod_a("chain_a.cppm", "export module A;\n" "export int val_a() { return 1; }\n"); + tmp.touch("chain_a.cppm", "export module A;\n" "export int val_a() { return 1; }\n"); + auto mod_a = tmp.path("chain_a.cppm"); // Module B: imports A. - TempFile mod_b("chain_b.cppm", - "export module B;\n" - "import A;\n" - "export int val_b() { return val_a() + 1; }\n"); + tmp.touch("chain_b.cppm", + "export module B;\n" + "import A;\n" + "export int val_b() { return val_a() + 1; }\n"); + auto mod_b = tmp.path("chain_b.cppm"); // Consumer: imports B (transitively needs A). - TempFile consumer("chain_consumer.cpp", "import B;\n" "int main() { return val_b(); }\n"); + tmp.touch("chain_consumer.cpp", "import B;\n" "int main() { return val_b(); }\n"); + auto consumer = tmp.path("chain_consumer.cpp"); WorkerHandle sl; ASSERT_TRUE(sl.spawn("stateless-worker")); @@ -119,14 +125,14 @@ TEST_CASE(BuildPCMChainThenCompile) { // Build PCM for A first. { worker::BuildPCMParams params; - params.file = mod_a.path; + params.file = mod_a; params.directory = "/tmp"; params.arguments = {"clang++", "-resource-dir", std::string(resource_dir()), "-std=c++20", "--precompile", - mod_a.path}; + mod_a}; params.module_name = "A"; auto result = co_await sl.peer->send_request(params); @@ -137,14 +143,14 @@ TEST_CASE(BuildPCMChainThenCompile) { // Build PCM for B, passing A's PCM (transitive dep). { worker::BuildPCMParams params; - params.file = mod_b.path; + params.file = mod_b; params.directory = "/tmp"; params.arguments = {"clang++", "-resource-dir", std::string(resource_dir()), "-std=c++20", "--precompile", - mod_b.path}; + mod_b}; params.module_name = "B"; params.pcms = { {"A", pcm_a} @@ -169,7 +175,7 @@ TEST_CASE(BuildPCMChainThenCompile) { sf.run([&]() -> et::task<> { worker::CompileParams params; - params.path = consumer.path; + params.path = consumer; params.version = 1; params.text = "import B;\n" "int main() { return val_b(); }\n"; params.directory = "/tmp"; @@ -178,7 +184,7 @@ TEST_CASE(BuildPCMChainThenCompile) { std::string(resource_dir()), "-std=c++20", "-fsyntax-only", - consumer.path}; + consumer}; // Clang needs ALL transitive PCMs. params.pcms = { {"A", pcm_a}, @@ -200,10 +206,13 @@ TEST_CASE(BuildPCMChainThenCompile) { } TEST_CASE(ModuleImplementationUnitWithWorker) { + TempDir tmp; // Module interface. - TempFile iface("impl_iface.cppm", "export module Calc;\n" "export int add(int a, int b);\n"); + tmp.touch("impl_iface.cppm", "export module Calc;\n" "export int add(int a, int b);\n"); + auto iface = tmp.path("impl_iface.cppm"); // Module implementation unit (no export). - TempFile impl("impl_unit.cpp", "module Calc;\n" "int add(int a, int b) { return a + b; }\n"); + tmp.touch("impl_unit.cpp", "module Calc;\n" "int add(int a, int b) { return a + b; }\n"); + auto impl = tmp.path("impl_unit.cpp"); // Build PCM for interface. WorkerHandle sl; @@ -214,14 +223,14 @@ TEST_CASE(ModuleImplementationUnitWithWorker) { sl.run([&]() -> et::task<> { worker::BuildPCMParams params; - params.file = iface.path; + params.file = iface; params.directory = "/tmp"; params.arguments = {"clang++", "-resource-dir", std::string(resource_dir()), "-std=c++20", "--precompile", - iface.path}; + iface}; params.module_name = "Calc"; auto result = co_await sl.peer->send_request(params); @@ -242,7 +251,7 @@ TEST_CASE(ModuleImplementationUnitWithWorker) { sf.run([&]() -> et::task<> { worker::CompileParams params; - params.path = impl.path; + params.path = impl; params.version = 1; params.text = "module Calc;\n" "int add(int a, int b) { return a + b; }\n"; params.directory = "/tmp"; @@ -251,7 +260,7 @@ TEST_CASE(ModuleImplementationUnitWithWorker) { std::string(resource_dir()), "-std=c++20", "-fsyntax-only", - impl.path}; + impl}; params.pcms = { {"Calc", pcm_path} }; diff --git a/tests/unit/server/stateful_worker_tests.cpp b/tests/unit/server/stateful_worker_tests.cpp index ded5244d..8fbbc301 100644 --- a/tests/unit/server/stateful_worker_tests.cpp +++ b/tests/unit/server/stateful_worker_tests.cpp @@ -24,7 +24,9 @@ TEST_CASE(SpawnAndExit) { } TEST_CASE(CompileRequest) { - TempFile src("compile_test.cpp", "int main() { return 0; }\n"); + TempDir tmp; + tmp.touch("compile_test.cpp", "int main() { return 0; }\n"); + auto src = tmp.path("compile_test.cpp"); WorkerHandle w; ASSERT_TRUE(w.spawn("stateful-worker")); @@ -33,11 +35,11 @@ TEST_CASE(CompileRequest) { w.run([&]() -> et::task<> { worker::CompileParams params; - params.path = src.path; + params.path = src; params.version = 1; params.text = "int main() { return 0; }\n"; params.directory = "/tmp"; - params.arguments = make_args(src.path); + params.arguments = make_args(src); params.pch = {"", 0}; params.pcms = {}; @@ -76,7 +78,9 @@ TEST_CASE(HoverWithoutCompile) { TEST_CASE(CompileThenHover) { std::string text = "int foo() { return 42; }\nint main() { return foo(); }\n"; - TempFile src("hover_test.cpp", text); + TempDir tmp; + tmp.touch("hover_test.cpp", text); + auto src = tmp.path("hover_test.cpp"); WorkerHandle w; ASSERT_TRUE(w.spawn("stateful-worker")); @@ -86,11 +90,11 @@ TEST_CASE(CompileThenHover) { w.run([&]() -> et::task<> { // First compile worker::CompileParams cp; - cp.path = src.path; + cp.path = src; cp.version = 1; cp.text = text; cp.directory = "/tmp"; - cp.arguments = make_args(src.path); + cp.arguments = make_args(src); auto compile_result = co_await w.peer->send_request(cp); CO_ASSERT_TRUE(compile_result.has_value()); @@ -98,7 +102,7 @@ TEST_CASE(CompileThenHover) { // After successful compilation, hover should return info. // "int foo() { return 42; }\n" is 25 chars, then char 22 on line 1 = offset 47 worker::HoverParams hp; - hp.path = src.path; + hp.path = src; hp.offset = 47; // position of 'foo' in 'return foo();' auto hover_result = co_await w.peer->send_request(hp); @@ -114,7 +118,9 @@ TEST_CASE(CompileThenHover) { } TEST_CASE(DocumentUpdate) { - TempFile src("update_test.cpp", "int x = 1;\n"); + TempDir tmp; + tmp.touch("update_test.cpp", "int x = 1;\n"); + auto src = tmp.path("update_test.cpp"); WorkerHandle w; ASSERT_TRUE(w.spawn("stateful-worker")); @@ -124,25 +130,25 @@ TEST_CASE(DocumentUpdate) { w.run([&]() -> et::task<> { // Compile first worker::CompileParams cp; - cp.path = src.path; + cp.path = src; cp.version = 1; cp.text = "int x = 1;\n"; cp.directory = "/tmp"; - cp.arguments = make_args(src.path); + cp.arguments = make_args(src); auto r1 = co_await w.peer->send_request(cp); CO_ASSERT_TRUE(r1.has_value()); // Send document update notification worker::DocumentUpdateParams up; - up.path = src.path; + up.path = src; up.version = 2; up.text = "int x = 2;\nint y = 3;\n"; w.peer->send_notification(up); // After update, hover still returns stale AST results (not null). worker::HoverParams hp; - hp.path = src.path; + hp.path = src; hp.offset = 4; auto hover_result = co_await w.peer->send_request(hp); @@ -299,13 +305,15 @@ TEST_CASE(InlayHintsWithoutCompile) { } TEST_CASE(MultipleSequentialRequests) { - TempFile src("seq_test.cpp", + TempDir tmp; + tmp.touch("seq_test.cpp", "int foo(int x) {\n" " return x + 1;\n" "}\n" "int main() {\n" " return foo(0);\n" "}\n"); + auto src = tmp.path("seq_test.cpp"); WorkerHandle w; ASSERT_TRUE(w.spawn("stateful-worker")); @@ -315,24 +323,24 @@ TEST_CASE(MultipleSequentialRequests) { w.run([&]() -> et::task<> { // Compile first so feature requests return real data. worker::CompileParams cp; - cp.path = src.path; + cp.path = src; cp.version = 1; cp.text = "int foo(int x) {\n return x + 1;\n}\nint main() {\n return foo(0);\n}\n"; cp.directory = "/tmp"; - cp.arguments = make_args(src.path); + cp.arguments = make_args(src); auto cr = co_await w.peer->send_request(cp); CO_ASSERT_TRUE(cr.has_value()); // Now send multiple different feature requests sequentially. worker::HoverParams hp; - hp.path = src.path; + hp.path = src; hp.offset = 4; // 'foo' on line 0 auto r1 = co_await w.peer->send_request(hp); EXPECT_TRUE(r1.has_value()); worker::CodeActionParams cap; - cap.path = src.path; + cap.path = src; auto r2 = co_await w.peer->send_request(cap); EXPECT_TRUE(r2.has_value()); @@ -340,18 +348,18 @@ TEST_CASE(MultipleSequentialRequests) { // lines: "int foo(int x) {\n"=17, " return x + 1;\n"=18, "}\n"=2, "int main() {\n"=14 // offset = 17+18+2+14+11 = 62 worker::GoToDefinitionParams gdp; - gdp.path = src.path; + gdp.path = src; gdp.offset = 62; auto r3 = co_await w.peer->send_request(gdp); EXPECT_TRUE(r3.has_value()); worker::SemanticTokensParams stp; - stp.path = src.path; + stp.path = src; auto r4 = co_await w.peer->send_request(stp); EXPECT_TRUE(r4.has_value()); worker::FoldingRangeParams frp; - frp.path = src.path; + frp.path = src; auto r5 = co_await w.peer->send_request(frp); EXPECT_TRUE(r5.has_value()); @@ -363,12 +371,15 @@ TEST_CASE(MultipleSequentialRequests) { } TEST_CASE(MultipleDocuments) { - std::vector> files; + TempDir tmp; + std::vector paths; std::vector texts; for(int i = 0; i < 3; i++) { + auto name = "multi_" + std::to_string(i) + ".cpp"; auto text = "int var_" + std::to_string(i) + " = " + std::to_string(i) + ";\n"; + tmp.touch(name, text); + paths.push_back(tmp.path(name)); texts.push_back(text); - files.push_back(std::make_unique("multi_" + std::to_string(i) + ".cpp", text)); } WorkerHandle w; @@ -380,11 +391,11 @@ TEST_CASE(MultipleDocuments) { // Compile 3 different documents. for(int i = 0; i < 3; i++) { worker::CompileParams cp; - cp.path = files[i]->path; + cp.path = paths[i]; cp.version = 1; cp.text = texts[i]; cp.directory = "/tmp"; - cp.arguments = make_args(files[i]->path); + cp.arguments = make_args(paths[i]); auto result = co_await w.peer->send_request(cp); EXPECT_TRUE(result.has_value()); @@ -393,7 +404,7 @@ TEST_CASE(MultipleDocuments) { // Hover on each document after compilation. for(int i = 0; i < 3; i++) { worker::HoverParams hp; - hp.path = files[i]->path; + hp.path = paths[i]; hp.offset = 4; // 'var_N' auto result = co_await w.peer->send_request(hp); @@ -436,7 +447,9 @@ TEST_CASE(EvictNotification) { } TEST_CASE(SpawnWithMemoryLimit) { - TempFile src("memlimit_test.cpp", "int memlimit_var = 42;\n"); + TempDir tmp; + tmp.touch("memlimit_test.cpp", "int memlimit_var = 42;\n"); + auto src = tmp.path("memlimit_test.cpp"); WorkerHandle w; // Spawn with a specific memory limit to test the CLI flag is accepted. @@ -447,18 +460,18 @@ TEST_CASE(SpawnWithMemoryLimit) { w.run([&]() -> et::task<> { // Compile first. worker::CompileParams cp; - cp.path = src.path; + cp.path = src; cp.version = 1; cp.text = "int memlimit_var = 42;\n"; cp.directory = "/tmp"; - cp.arguments = make_args(src.path); + cp.arguments = make_args(src); auto cr = co_await w.peer->send_request(cp); EXPECT_TRUE(cr.has_value()); // Feature request should work after compilation. worker::HoverParams hp; - hp.path = src.path; + hp.path = src; hp.offset = 4; // 'memlimit_var' auto result = co_await w.peer->send_request(hp); diff --git a/tests/unit/server/stateless_worker_tests.cpp b/tests/unit/server/stateless_worker_tests.cpp index bd2f826a..0edd6072 100644 --- a/tests/unit/server/stateless_worker_tests.cpp +++ b/tests/unit/server/stateless_worker_tests.cpp @@ -83,7 +83,9 @@ TEST_CASE(SpawnAndExit) { } TEST_CASE(BuildPCHRequest) { - TempFile hdr("test_pch.h", "#pragma once\nint pch_global = 42;\n"); + TempDir tmp; + tmp.touch("test_pch.h", "#pragma once\nint pch_global = 42;\n"); + auto hdr = tmp.path("test_pch.h"); WorkerHandle w; ASSERT_TRUE(w.spawn("stateless-worker")); @@ -92,10 +94,10 @@ TEST_CASE(BuildPCHRequest) { w.run([&]() -> et::task<> { worker::BuildPCHParams params; - params.file = hdr.path; + params.file = hdr; params.directory = "/tmp"; params.arguments = - {"clang++", "-resource-dir", std::string(resource_dir()), "-x", "c++-header", hdr.path}; + {"clang++", "-resource-dir", std::string(resource_dir()), "-x", "c++-header", hdr}; params.content = "#pragma once\nint pch_global = 42;\n"; auto result = co_await w.peer->send_request(params); @@ -108,7 +110,9 @@ TEST_CASE(BuildPCHRequest) { } TEST_CASE(IndexRequest) { - TempFile src("test_index.cpp", "int indexed_var = 1;\n"); + TempDir tmp; + tmp.touch("test_index.cpp", "int indexed_var = 1;\n"); + auto src = tmp.path("test_index.cpp"); WorkerHandle w; ASSERT_TRUE(w.spawn("stateless-worker")); @@ -117,9 +121,9 @@ TEST_CASE(IndexRequest) { w.run([&]() -> et::task<> { worker::IndexParams params; - params.file = src.path; + params.file = src; params.directory = "/tmp"; - params.arguments = make_args(src.path); + params.arguments = make_args(src); auto result = co_await w.peer->send_request(params); EXPECT_TRUE(result.has_value()); @@ -139,8 +143,10 @@ TEST_CASE(IndexRequest) { TEST_SUITE(StatelessWorkerExtended) { TEST_CASE(BuildPCMRequest) { - TempFile src("test_module.cppm", - "export module test_module;\nexport int module_func() { return 1; }\n"); + TempDir tmp; + tmp.touch("test_module.cppm", + "export module test_module;\nexport int module_func() { return 1; }\n"); + auto src = tmp.path("test_module.cppm"); WorkerHandle w; ASSERT_TRUE(w.spawn("stateless-worker")); @@ -149,14 +155,14 @@ TEST_CASE(BuildPCMRequest) { w.run([&]() -> et::task<> { worker::BuildPCMParams params; - params.file = src.path; + params.file = src; params.directory = "/tmp"; params.arguments = {"clang++", "-resource-dir", std::string(resource_dir()), "-std=c++20", "--precompile", - src.path}; + src}; params.module_name = "test_module"; auto result = co_await w.peer->send_request(params); @@ -170,7 +176,9 @@ TEST_CASE(BuildPCMRequest) { TEST_CASE(CompletionRequest) { std::string text = "int foo = 1;\nint bar = fo"; - TempFile src("completion_test.cpp", text); + TempDir tmp; + tmp.touch("completion_test.cpp", text); + auto src = tmp.path("completion_test.cpp"); WorkerHandle w; ASSERT_TRUE(w.spawn("stateless-worker")); @@ -179,11 +187,11 @@ TEST_CASE(CompletionRequest) { w.run([&]() -> et::task<> { worker::CompletionParams params; - params.path = src.path; + params.path = src; params.version = 1; params.text = text; params.directory = "/tmp"; - params.arguments = make_args(src.path); + params.arguments = make_args(src); params.offset = 25; // after "fo" in "int bar = fo" (13 + 12) auto result = co_await w.peer->send_request(params); @@ -197,7 +205,9 @@ TEST_CASE(CompletionRequest) { TEST_CASE(SignatureHelpRequest) { std::string text = "void foo(int a, int b) {}\nint main() { foo("; - TempFile src("sighelp_test.cpp", text); + TempDir tmp; + tmp.touch("sighelp_test.cpp", text); + auto src = tmp.path("sighelp_test.cpp"); WorkerHandle w; ASSERT_TRUE(w.spawn("stateless-worker")); @@ -206,11 +216,11 @@ TEST_CASE(SignatureHelpRequest) { w.run([&]() -> et::task<> { worker::SignatureHelpParams params; - params.path = src.path; + params.path = src; params.version = 1; params.text = text; params.directory = "/tmp"; - params.arguments = make_args(src.path); + params.arguments = make_args(src); params.offset = 45; // after "foo(" (26 + 19) auto result = co_await w.peer->send_request(params); @@ -224,11 +234,13 @@ TEST_CASE(SignatureHelpRequest) { } TEST_CASE(MultipleStatelessRequests) { - std::vector> files; + TempDir tmp; + std::vector paths; for(int i = 0; i < 3; i++) { + auto name = "multi_index_" + std::to_string(i) + ".cpp"; auto text = "int idx_var_" + std::to_string(i) + " = " + std::to_string(i) + ";\n"; - files.push_back( - std::make_unique("multi_index_" + std::to_string(i) + ".cpp", text)); + tmp.touch(name, text); + paths.push_back(tmp.path(name)); } WorkerHandle w; @@ -240,9 +252,9 @@ TEST_CASE(MultipleStatelessRequests) { // Send multiple index requests to test stateless worker handles them sequentially. for(int i = 0; i < 3; i++) { worker::IndexParams params; - params.file = files[i]->path; + params.file = paths[i]; params.directory = "/tmp"; - params.arguments = make_args(files[i]->path); + params.arguments = make_args(paths[i]); auto result = co_await w.peer->send_request(params); EXPECT_TRUE(result.has_value()); diff --git a/tests/unit/server/worker_test_helpers.h b/tests/unit/server/worker_test_helpers.h index bb2954c8..bda6c2e4 100644 --- a/tests/unit/server/worker_test_helpers.h +++ b/tests/unit/server/worker_test_helpers.h @@ -1,8 +1,6 @@ #pragma once #include -#include -#include #include #ifndef _WIN32 @@ -10,6 +8,7 @@ #include #endif +#include "test/temp_dir.h" #include "command/argument_parser.h" #include "command/command.h" #include "eventide/async/async.h" @@ -47,31 +46,6 @@ inline std::string clice_binary() { return std::string(path); } -/// RAII temporary file: writes content to disk, removes on destruction. -struct TempFile { - std::string path; - - TempFile(const std::string& name, const std::string& content) { - llvm::SmallString<256> tmp_dir; - llvm::sys::path::system_temp_directory(true, tmp_dir); - llvm::sys::path::append(tmp_dir, "clice_test_" + name); - path = std::string(tmp_dir); - std::ofstream ofs(path); - ofs << content; - } - - ~TempFile() { - std::remove(path.c_str()); - } - - std::string uri() const { - return "file://" + path; - } - - TempFile(const TempFile&) = delete; - TempFile& operator=(const TempFile&) = delete; -}; - /// Build compile arguments for a source file, including -resource-dir. inline std::vector make_args(const std::string& file_path, const std::string& extra = "") { diff --git a/tests/unit/syntax/dependency_graph_tests.cpp b/tests/unit/syntax/dependency_graph_tests.cpp index 4b7a79c9..e557e4f7 100644 --- a/tests/unit/syntax/dependency_graph_tests.cpp +++ b/tests/unit/syntax/dependency_graph_tests.cpp @@ -189,12 +189,6 @@ TEST_CASE(EmptyIncludes) { // scan_dependency_graph() integration tests // ============================================================================ -/// Write a compile_commands.json into the temp dir and load it into the given CDB. -void write_cdb(TempDir& tmp, CompilationDatabase& cdb, llvm::StringRef json_content) { - tmp.touch("compile_commands.json", json_content); - cdb.load(tmp.path("compile_commands.json")); -} - TEST_SUITE(ScanDependencyGraph) { TEST_CASE(EmptyCDB) { diff --git a/tests/unit/test/cdb_helper.h b/tests/unit/test/cdb_helper.h index c933b658..5fbcb144 100644 --- a/tests/unit/test/cdb_helper.h +++ b/tests/unit/test/cdb_helper.h @@ -4,6 +4,9 @@ #include #include +#include "test/temp_dir.h" +#include "command/command.h" + #include "llvm/ADT/ArrayRef.h" #include "llvm/ADT/StringRef.h" @@ -55,4 +58,10 @@ inline std::string build_cdb_json(llvm::ArrayRef entries) { return json; } +/// Write a compile_commands.json into the temp dir and load it into the given CDB. +inline void write_cdb(TempDir& tmp, CompilationDatabase& cdb, llvm::StringRef json_content) { + tmp.touch("compile_commands.json", json_content); + cdb.load(tmp.path("compile_commands.json")); +} + } // namespace clice::testing