Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,39 @@ jobs:

- name: Run TypeScript integration tests
run: go test ./integrationtests/tests/typescript/...

clangd-integration-tests:
name: Clangd Integration Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.24"
check-latest: true
cache: true

- name: Install LLVM, Clang and bear
run: |
sudo apt-get update
sudo apt-get install -y clang-16 llvm-16 clangd-16 bear

- name: Verify Clangd Installation
run: |
sudo ln -s /usr/bin/clangd-16 /usr/bin/clangd
clangd-16 --version
clangd --version

- name: Create compile commands
run: |
cd integrationtests/workspaces/clangd
bear -- make
cd ../../../..

- name: Run Clangd definition tests
run: go test ./integrationtests/tests/clangd/definition...

- name: Run Clangd diagnostics tests
run: go test ./integrationtests/tests/clangd/diagnostics...
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ mcp-language-server
# Test output
test-output/
*.diff
integrationtests/workspaces/clangd/compile_commands.json
integrationtests/workspaces/clangd/src/*.o
integrationtests/workspaces/clangd/clean_program
integrationtests/workspaces/clangd/program
integrationtests/workspaces/clangd/.cache


# Temporary files
*~
Expand Down
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,37 @@ This is an [MCP](https://modelcontextprotocol.io/introduction) server that runs
</pre>
</div>
</details>
<details>
<summary>C/C++ (clangd)</summary>
<div>
<p><strong>Install clangd</strong>: Download prebuilt binaries from the <a href="https://github.com/clangd/clangd/releases">official LLVM releases page</a> or install via your system's package manager (e.g., <code>apt install clangd</code>, <code>brew install clangd</code>).</p>
<p><strong>Configure your MCP client</strong>: This will be different but similar for each client. For Claude Desktop, add the following to <code>~/Library/Application\\ Support/Claude/claude_desktop_config.json</code></p>

<pre>
{
"mcpServers": {
"language-server": {
"command": "mcp-language-server",
"args": [
"--workspace",
"/Users/you/dev/yourproject/",
"--lsp",
"/path/to/your/clangd_binary",
"--",
"--compile-commands-dir=/path/to/yourproject/build_or_compile_commands_dir"
]
}
}
}
</pre>
<p><strong>Note</strong>:</p>
<ul>
<li>Replace <code>/path/to/your/clangd_binary</code> with the actual path to your clangd executable.</li>
<li><code>--compile-commands-dir</code> should point to the directory containing your <code>compile_commands.json</code> file (e.g., <code>./build</code>, <code>./cmake-build-debug</code>).</li>
<li>Ensure <code>compile_commands.json</code> is generated for your project for clangd to work effectively.</li>
</ul>
</div>
</details>
<details>
<summary>Other</summary>
<div>
Expand Down
11 changes: 11 additions & 0 deletions integrationtests/snapshots/clangd/definition/class.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---

Symbol: TestClass
/TEST_OUTPUT/workspace/src/consumer.cpp
Range: L6:C1 - L9:C2

6|class TestClass {
7| public:
8| void method() {}
9|};

8 changes: 8 additions & 0 deletions integrationtests/snapshots/clangd/definition/constant.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---

Symbol: TEST_CONSTANT
/TEST_OUTPUT/workspace/src/helper.cpp
Range: L4:C1 - L4:C29

4|const int TEST_CONSTANT = 42;

11 changes: 11 additions & 0 deletions integrationtests/snapshots/clangd/definition/foobar.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---

Symbol: foo_bar
/TEST_OUTPUT/workspace/src/main.cpp
Range: L5:C1 - L8:C2

5|void foo_bar() {
6| std::cout << "Hello, World!" << std::endl;
7| return;
8|}

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---

Symbol: helperFunction
/TEST_OUTPUT/workspace/clangd/include/helper.hpp
Range: L1:C1 - L1:C22

1|void helperFunction();

11 changes: 11 additions & 0 deletions integrationtests/snapshots/clangd/definition/method.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---

Symbol: method
/TEST_OUTPUT/workspace/src/consumer.cpp
Range: L6:C1 - L9:C2

6|class TestClass {
7| public:
8| void method() {}
9|};

10 changes: 10 additions & 0 deletions integrationtests/snapshots/clangd/definition/struct.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---

Symbol: TestStruct
/TEST_OUTPUT/workspace/src/types.cpp
Range: L6:C1 - L8:C2

6|struct TestStruct {
7| int value;
8|};

8 changes: 8 additions & 0 deletions integrationtests/snapshots/clangd/definition/type.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---

Symbol: TestType
/TEST_OUTPUT/workspace/src/types.cpp
Range: L10:C1 - L10:C21

10|using TestType = int;

8 changes: 8 additions & 0 deletions integrationtests/snapshots/clangd/definition/variable.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---

Symbol: TEST_VARIABLE
/TEST_OUTPUT/workspace/src/helper.cpp
Range: L5:C1 - L5:C24

5|int TEST_VARIABLE = 100;

1 change: 1 addition & 0 deletions integrationtests/snapshots/clangd/diagnostics/clean.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/TEST_OUTPUT/workspace/src/clean.cpp
10 changes: 10 additions & 0 deletions integrationtests/snapshots/clangd/diagnostics/unreachable.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/TEST_OUTPUT/workspace/src/main.cpp
Diagnostics in File: 1
WARNING at L15:C38: Code will never be executed (Source: clang, Code: -Wunreachable-code)

10|int main() {
...
13|
14| // Intentional error: unreachable code
15| std::cout << "This is unreachable" << std::endl;
16|}
19 changes: 19 additions & 0 deletions integrationtests/tests/clangd/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
\
# Clangd Integration Tests

This directory contains integration tests for `clangd`, the C/C++ language server.

## Prerequisites

Before running these tests, you must generate the `compile_commands.json` file in the `integrationtests/workspaces/clangd` directory. This can typically be done by navigating to that directory and running a tool like `bear` with your build command (e.g., `bear -- make`).

The GitHub Actions workflow for these tests uses the following command from the root of the repository:
```bash
cd integrationtests/workspaces/clangd
bear -- make
cd ../../../..
```

## Clangd Version

These tests are currently run against **clangd version 16**. While they may pass with other versions of clangd, compatibility is not guaranteed.
176 changes: 176 additions & 0 deletions integrationtests/tests/clangd/definition/definition_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
package definition_test

import (
"context"
"path/filepath"
"strings"
"testing"
"time"

"github.com/isaacphi/mcp-language-server/integrationtests/tests/clangd/internal"
"github.com/isaacphi/mcp-language-server/integrationtests/tests/common"
"github.com/isaacphi/mcp-language-server/internal/tools"
)

// TestReadDefinition tests the ReadDefinition tool with various C++ type definitions
func TestReadDefinition(t *testing.T) {
// Helper function to open all files and wait for indexing
openAllFilesAndWait := func(suite *common.TestSuite, ctx context.Context) {
// Open all files to ensure clangd indexes everything
filesToOpen := []string{
"src/main.cpp",
"src/types.cpp",
"src/helper.cpp",
"src/consumer.cpp",
"src/another_consumer.cpp",
"src/clean.cpp",
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it work without this here? Opening all files was a hack I added for the typescript language server only but other language servers don't need to open every file, only the project

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, the tests were failing without it because clangd does not start indexing until the first time a file is opened (clangd discussion). Then it will eventually do background indexing for other files defined in compile_commands.json. I actually had a bug with how I was invoking clangd previously, which I have now fixed. We only need to open a single file for clangd to index all the files when we correctly pass it the compile-commands-dir.

Right now, the most robust way to do the tests is to open any file and wait, but I think long-term, using LSP progress notifications would be better. However, this requires larger changes to the code base. I am not an LSP expert, so I used Gemini to get a sense of how that may work:
https://g.co/gemini/share/3648aef3aa59


for _, file := range filesToOpen {
filePath := filepath.Join(suite.WorkspaceDir, file)
err := suite.Client.OpenFile(ctx, filePath)
if err != nil {
// Don't fail the test, some files might not exist in certain tests
t.Logf("Note: Failed to open %s: %v", file, err)
}
}
// Wait for indexing to complete. clangd won't index files until they are opened.
time.Sleep(10 * time.Second)
}

suite := internal.GetTestSuite(t)

ctx, cancel := context.WithTimeout(suite.Context, 10*time.Second)
defer cancel()

// Open all files and wait for clangd to index them
openAllFilesAndWait(suite, ctx)

tests := []struct {
name string
symbolName string
expectedText string
snapshotName string
}{
{
name: "Function",
symbolName: "foo_bar",
expectedText: "void foo_bar()",
snapshotName: "foobar",
},
{
name: "Class",
symbolName: "TestClass",
expectedText: "class TestClass",
snapshotName: "class",
},
{
name: "Method",
symbolName: "method",
expectedText: "void method()",
snapshotName: "method",
},
{
name: "Struct",
symbolName: "TestStruct",
expectedText: "struct TestStruct",
snapshotName: "struct",
},
{
name: "Type",
symbolName: "TestType",
expectedText: "using TestType",
snapshotName: "type",
},
{
name: "Constant",
symbolName: "TEST_CONSTANT",
expectedText: "const int TEST_CONSTANT",
snapshotName: "constant",
},
{
name: "Variable",
symbolName: "TEST_VARIABLE",
expectedText: "int TEST_VARIABLE",
snapshotName: "variable",
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Call the ReadDefinition tool
result, err := tools.ReadDefinition(ctx, suite.Client, tc.symbolName)
if err != nil {
t.Fatalf("Failed to read definition: %v", err)
}

// Check that the result contains relevant information
if !strings.Contains(result, tc.expectedText) {
t.Errorf("Definition does not contain expected text: %s", tc.expectedText)
}

// Use snapshot testing to verify exact output
common.SnapshotTest(t, "clangd", "definition", tc.snapshotName, result)
})
}
}

func TestReadDefinitionInAnotherFile(t *testing.T) {
// Helper function to open all files and wait for indexing
openAllFilesAndWait := func(suite *common.TestSuite, ctx context.Context) {
// Open all files to ensure clangd indexes everything
filesToOpen := []string{
"src/main.cpp",
}

for _, file := range filesToOpen {
filePath := filepath.Join(suite.WorkspaceDir, file)
err := suite.Client.OpenFile(ctx, filePath)
if err != nil {
// Don't fail the test, some files might not exist in certain tests
t.Logf("Note: Failed to open %s: %v", file, err)
}
}
time.Sleep(5 * time.Second)
}

suite := internal.GetTestSuite(t)

ctx, cancel := context.WithTimeout(suite.Context, 10*time.Second)
defer cancel()

// Open all files and wait for clangd to index them
openAllFilesAndWait(suite, ctx)

tests := []struct {
name string
symbolName string
expectedText string
snapshotName string
}{
{
name: "Function",
symbolName: "helperFunction",
expectedText: "void helperFunction()",
snapshotName: "helperFunction",
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Call the ReadDefinition tool
result, err := tools.ReadDefinition(ctx, suite.Client, tc.symbolName)
if err != nil {
t.Fatalf("Failed to read definition: %v", err)
}

// Check that the result contains relevant information
if !strings.Contains(result, tc.expectedText) {
t.Errorf("Definition does not contain expected text: %s", tc.expectedText)
}

// Use snapshot testing to verify exact output
common.SnapshotTest(t, "clangd", "definition", tc.snapshotName, result)
})
}
}
Loading
Loading