Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
10 changes: 8 additions & 2 deletions .github/workflows/CI.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -57,5 +57,11 @@ jobs:
- name: Run nix flake check
run: nix flake check --all-systems

- name: Run integration tests
run: cargo test -- --show-output
- name: Run integration tests with coverage
run: run-cov -- --show-output

- name: Upload to codecov.io
uses: codecov/codecov-action@v5
with:
token: ${{secrets.CODECOV_TOKEN}}
fail_ci_if_error: github.actor != 'dependabot[bot]'
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ All notable changes to this project will be documented in this file.
implementation with more comprehensive event handling
- **Test Performance**: Optimized integration tests with better timing and
one-time binary compilation for improved CI performance
- **Code Coverage**: Use **grcov** for LLVM-based code coverage
with HTML, Cobertura, and Markdown report generation
- **CI Coverage Integration**: Added codecov.io integration with automated
coverage reporting in GitHub Actions

## [v0.5.0] - 2025-08-20

Expand Down
25 changes: 19 additions & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,19 +63,28 @@ nix develop .

```bash
# Run all tests
cargo test -- --show-output
./scripts/run-test.sh -- --show-output

# Run single specific module test
cargo test -- --show-output neovim::integration_tests
./scripts/run-test.sh -- --show-output neovim::integration_tests

# Run single specific test
cargo test -- --show-output neovim::integration_tests::test_tcp_connection_lifecycle
./scripts/run-test.sh -- --show-output neovim::integration_tests::test_tcp_connection_lifecycle

# Skip integration tests (which require Neovim)
cargo test -- --skip=integration_tests --show-output 1
./scripts/run-test.sh -- --skip=integration_tests --show-output

# Run tests with coverage using grcov
nix run .#cov -- --show-output

# Run specific tests with coverage
nix run .#cov -- --show-output neovim::integration_tests

# Run tests in Nix environment (requires IN_NIX_SHELL not set)
nix develop . --command cargo test -- --show-output 1
nix develop . --command ./scripts/run-test.sh -- --show-output

# Alternative: Use nix test app
nix run .#test -- --show-output
```

**Note**: The `nix develop . --command` syntax only works when the
Expand Down Expand Up @@ -492,6 +501,7 @@ the new `lua_tools.rs` module:
**Testing and Development Dependencies:**

- **`tempfile`**: Temporary file and directory management for integration tests
- **`grcov`**: Code coverage reporting tool using LLVM instrumentation
- **Enhanced deserialization**: Support for both string and struct formats
in CodeAction and WorkspaceEdit types
- **Lua tool testing** ⚠️ **(Experimental)**: Integration tests for custom tool registration
Expand All @@ -512,6 +522,8 @@ the new `lua_tools.rs` module:
- **Enhanced reliability**: Robust LSP synchronization with notification tracking
- **Optimized timing**: Better test performance with improved setup and teardown
- **Notification testing**: Unit tests for notification tracking system
- **Code coverage**: LLVM-based code coverage using grcov with HTML, Cobertura,
and Markdown report generation

## Error Handling

Expand Down Expand Up @@ -609,7 +621,8 @@ use rmcp::{ErrorData as McpError, /* other MCP types */};
This project uses Nix flakes for reproducible development environments.
The flake provides:

- Rust toolchain (stable) with clippy, rustfmt, and rust-analyzer
- Rust toolchain (stable) with clippy, rustfmt, rust-analyzer, and LLVM tools
- grcov for code coverage analysis
- Neovim 0.11.3+ for integration testing
- Pre-commit hooks for code quality

Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -530,8 +530,14 @@ cargo test -- --show-output
# Skip integration tests (which require Neovim)
cargo test -- --skip=integration_tests --show-output

# Run tests with coverage reporting
nix run .#cov -- --show-output

# In Nix environment
nix develop . --command cargo test -- --show-output

# Alternative test runner
nix run .#test -- --show-output
```

**Note**: If already in a Nix shell, omit the `nix develop . --command` prefix.
Expand Down
101 changes: 65 additions & 36 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -46,42 +46,6 @@
);
git_last_modified = toString self.sourceInfo.lastModified or "unknown";
in {
devShells = {
default = pkgs.mkShell {
nativeBuildInputs = with pkgs; [
(fenix.stable.withComponents [
"cargo"
"clippy"
"rust-src"
"rustc"
"rustfmt"
])
];
packages = with pkgs; [
# Development
rust-analyzer-nightly
pre-commit

# Integration tests
neovim-unwrapped
lua-language-server

go
gopls

zig
zls

typescript
typescript-language-server
];
shellHook = ''
# Unset SOURCE_DATE_EPOCH to prevent reproducible build timestamps during development.
# This allows timestamps to reflect the current time, which is useful for development workflows.
unset SOURCE_DATE_EPOCH
'';
};
};
packages = rec {
default = nvim-mcp;
nvim-mcp = let
Expand Down Expand Up @@ -111,6 +75,14 @@
"--skip=integration_tests"
];
};
run-test = pkgs.writeShellApplication {
name = "run-test";
text = builtins.readFile ./scripts/run-test.sh;
};
run-cov = pkgs.writeShellApplication {
name = "run-cov";
text = builtins.readFile ./scripts/run-cov.sh;
};
};
apps = {
default = {
Expand All @@ -120,6 +92,63 @@
};
program = lib.getExe self.packages.${system}.nvim-mcp;
};
test = {
type = "app";
meta = {
description = "Run tests";
};
program = lib.getExe self.packages.${system}.run-test;
};
cov = {
type = "app";
meta = {
description = "Run tests with coverage";
};
program = lib.getExe self.packages.${system}.run-cov;
};
};
devShells = {
default = pkgs.mkShell {
nativeBuildInputs = with pkgs; [
(fenix.stable.withComponents [
"cargo"
"clippy"
"rust-src"
"rustc"
"rustfmt"
"llvm-tools"
])
];
packages = with pkgs;
[
# Development
rust-analyzer-nightly
grcov
pre-commit

# Integration tests
neovim-unwrapped
lua-language-server

go
gopls

zig
zls

typescript
typescript-language-server
]
++ (with self.packages.${system}; [
run-test
run-cov
]);
shellHook = ''
# Unset SOURCE_DATE_EPOCH to prevent reproducible build timestamps during development.
# This allows timestamps to reflect the current time, which is useful for development workflows.
unset SOURCE_DATE_EPOCH
'';
};
};
}
);
Expand Down
20 changes: 20 additions & 0 deletions scripts/run-cov.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export RUSTFLAGS="-Cinstrument-coverage"
export CARGO_TARGET_DIR="./target/coverage"
export LLVM_PROFILE_FILE="${CARGO_TARGET_DIR}/data/nvim-mcp-%p-%m.profraw"

cargo build --bin nvim-mcp
cargo test "$@"

echo "Generating code coverage report..."

mkdir -p ${CARGO_TARGET_DIR}/result/
grcov ${CARGO_TARGET_DIR}/data \
--llvm \
--branch \
--source-dir . \
--ignore-not-existing \
--ignore '../*' --ignore "/*" \
--binary-path ${CARGO_TARGET_DIR}/debug/ \
--output-types html,cobertura,markdown \
--output-path ${CARGO_TARGET_DIR}/result/
tail -n 1 ${CARGO_TARGET_DIR}/result/markdown.md
2 changes: 2 additions & 0 deletions scripts/run-test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
cargo build --bin nvim-mcp
cargo test "$@"
4 changes: 2 additions & 2 deletions src/neovim/integration_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ async fn setup_lsp_with_analysis(
client: &mut NeovimClient<impl tokio::io::AsyncRead + tokio::io::AsyncWrite + Send + Unpin>,
) -> Result<(), NeovimError> {
client.setup_autocmd().await?;
wait_for_lsp_analysis_complete(client, 5000).await?;
wait_for_lsp_analysis_complete(client, 15000).await?;
Ok(())
}

Expand All @@ -34,7 +34,7 @@ async fn setup_lsp_ready_only(
client: &mut NeovimClient<impl tokio::io::AsyncRead + tokio::io::AsyncWrite + Send + Unpin>,
) -> Result<(), NeovimError> {
client.setup_autocmd().await?;
client.wait_for_lsp_ready(None, 5000).await?;
client.wait_for_lsp_ready(None, 15000).await?;
Ok(())
}

Expand Down
64 changes: 25 additions & 39 deletions src/server/integration_tests.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
use std::path::PathBuf;
use std::sync::OnceLock;

use rmcp::{
model::{CallToolRequestParam, ReadResourceRequestParam},
Expand All @@ -13,52 +12,39 @@ use tracing_test::traced_test;

use crate::test_utils::*;

static BINARY_PATH: OnceLock<PathBuf> = OnceLock::new();

/// Get the compiled binary path, compiling only once
fn get_compiled_binary() -> PathBuf {
BINARY_PATH
.get_or_init(|| {
info!("Compiling nvim-mcp binary (one-time compilation)...");

// Build the binary using cargo build
let output = std::process::Command::new("cargo")
.args(["build", "--bin", "nvim-mcp"])
.output()
.expect("Failed to execute cargo build");

if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
panic!("Failed to compile nvim-mcp binary: {}", stderr);
}

// Determine the binary path
let manifest_dir =
std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR not set");
let mut binary_path = PathBuf::from(manifest_dir);
binary_path.push("target");
binary_path.push("debug");
binary_path.push("nvim-mcp");

// On Windows, add .exe extension
#[cfg(windows)]
binary_path.set_extension("exe");

if !binary_path.exists() {
panic!("Binary not found at expected path: {:?}", binary_path);
}

info!("Binary compiled successfully at: {:?}", binary_path);
let mut binary_path = get_target_dir();
binary_path.push("debug");
binary_path.push("nvim-mcp");
if !binary_path.exists() {
panic!(
"Compiled binary not found at {:?}. Please run `cargo build` first.",
binary_path
);
}

binary_path
}

fn get_target_dir() -> PathBuf {
std::env::var("CARGO_TARGET_DIR")
.map(PathBuf::from)
.or_else(|_| {
// Default to target directory if not set
std::env::var("CARGO_MANIFEST_DIR").map(|dir| {
let mut path = PathBuf::from(dir);
path.push("target");
path
})
})
.clone()
.expect("Failed to determine target directory")
}

/// Macro to create an MCP service using the pre-compiled binary
macro_rules! create_mcp_service {
() => {{
let binary_path = get_compiled_binary();
().serve(TokioChildProcess::new(Command::new(binary_path))?)
let command = Command::new(get_compiled_binary());
().serve(TokioChildProcess::new(command)?)
.await
.map_err(|e| {
error!("Failed to connect to server: {}", e);
Expand Down