Unnamed repository; edit this file 'description' to name the repository.
internal: Landing integration test infra for proc-macro-srv-cli
Lukas Wirth 3 months ago
parent 4341269 · commit 72e5894
-rw-r--r--Cargo.lock6
-rw-r--r--crates/proc-macro-api/src/lib.rs6
-rw-r--r--crates/proc-macro-srv-cli/Cargo.toml14
-rw-r--r--crates/proc-macro-srv-cli/src/lib.rs6
-rw-r--r--crates/proc-macro-srv-cli/src/main.rs43
-rw-r--r--crates/proc-macro-srv-cli/src/main_loop.rs5
-rw-r--r--crates/proc-macro-srv-cli/tests/common/utils.rs213
-rw-r--r--crates/proc-macro-srv-cli/tests/legacy_json.rs224
8 files changed, 494 insertions, 23 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 5bdde7c7c3..d6c6250e13 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1879,9 +1879,15 @@ name = "proc-macro-srv-cli"
version = "0.0.0"
dependencies = [
"clap",
+ "expect-test",
+ "intern",
+ "paths",
"postcard",
"proc-macro-api",
"proc-macro-srv",
+ "proc-macro-test",
+ "span",
+ "tt",
]
[[package]]
diff --git a/crates/proc-macro-api/src/lib.rs b/crates/proc-macro-api/src/lib.rs
index 98ee6817c2..822809943a 100644
--- a/crates/proc-macro-api/src/lib.rs
+++ b/crates/proc-macro-api/src/lib.rs
@@ -44,10 +44,14 @@ pub mod version {
pub const CURRENT_API_VERSION: u32 = HASHED_AST_ID;
}
-#[derive(Copy, Clone)]
+/// Protocol format for communication between client and server.
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum ProtocolFormat {
+ /// JSON-based legacy protocol (newline-delimited JSON).
JsonLegacy,
+ /// Postcard-based legacy protocol (COBS-encoded postcard).
PostcardLegacy,
+ /// Bidirectional postcard protocol with sub-request support.
BidirectionalPostcardPrototype,
}
diff --git a/crates/proc-macro-srv-cli/Cargo.toml b/crates/proc-macro-srv-cli/Cargo.toml
index 6b2db0b269..a25e3b64ad 100644
--- a/crates/proc-macro-srv-cli/Cargo.toml
+++ b/crates/proc-macro-srv-cli/Cargo.toml
@@ -10,12 +10,26 @@ license.workspace = true
rust-version.workspace = true
publish = false
+[lib]
+doctest = false
+
[dependencies]
proc-macro-srv.workspace = true
proc-macro-api.workspace = true
postcard.workspace = true
clap = {version = "4.5.42", default-features = false, features = ["std"]}
+[dev-dependencies]
+expect-test.workspace = true
+paths.workspace = true
+# span = {workspace = true, default-features = false} does not work
+span = { path = "../span", default-features = false}
+tt.workspace = true
+intern.workspace = true
+
+# used as proc macro test target
+proc-macro-test.path = "../proc-macro-srv/proc-macro-test"
+
[features]
default = []
# default = ["sysroot-abi"]
diff --git a/crates/proc-macro-srv-cli/src/lib.rs b/crates/proc-macro-srv-cli/src/lib.rs
new file mode 100644
index 0000000000..9e6f03bf46
--- /dev/null
+++ b/crates/proc-macro-srv-cli/src/lib.rs
@@ -0,0 +1,6 @@
+//! Library interface for `proc-macro-srv-cli`.
+//!
+//! This module exposes the server main loop and protocol format for integration testing.
+
+#[cfg(feature = "sysroot-abi")]
+pub mod main_loop;
diff --git a/crates/proc-macro-srv-cli/src/main.rs b/crates/proc-macro-srv-cli/src/main.rs
index 189a1eea5c..a246d4d3f2 100644
--- a/crates/proc-macro-srv-cli/src/main.rs
+++ b/crates/proc-macro-srv-cli/src/main.rs
@@ -9,11 +9,11 @@ extern crate rustc_driver as _;
mod version;
-#[cfg(feature = "sysroot-abi")]
-mod main_loop;
use clap::{Command, ValueEnum};
+use proc_macro_api::ProtocolFormat;
+
#[cfg(feature = "sysroot-abi")]
-use main_loop::run;
+use proc_macro_srv_cli::main_loop::run;
fn main() -> std::io::Result<()> {
let v = std::env::var("RUST_ANALYZER_INTERNALS_DO_NOT_USE");
@@ -32,7 +32,7 @@ fn main() -> std::io::Result<()> {
.long("format")
.action(clap::ArgAction::Set)
.default_value("json-legacy")
- .value_parser(clap::builder::EnumValueParser::<ProtocolFormat>::new()),
+ .value_parser(clap::builder::EnumValueParser::<ProtocolFormatArg>::new()),
clap::Arg::new("version")
.long("version")
.action(clap::ArgAction::SetTrue)
@@ -43,33 +43,37 @@ fn main() -> std::io::Result<()> {
println!("rust-analyzer-proc-macro-srv {}", version::version());
return Ok(());
}
- let &format =
- matches.get_one::<ProtocolFormat>("format").expect("format value should always be present");
+ let &format = matches
+ .get_one::<ProtocolFormatArg>("format")
+ .expect("format value should always be present");
let mut stdin = std::io::BufReader::new(std::io::stdin());
let mut stdout = std::io::stdout();
- run(&mut stdin, &mut stdout, format)
+ run(&mut stdin, &mut stdout, format.into())
}
+/// Wrapper for CLI argument parsing that implements `ValueEnum`.
#[derive(Copy, Clone)]
-enum ProtocolFormat {
- JsonLegacy,
- PostcardLegacy,
- BidirectionalPostcardPrototype,
+struct ProtocolFormatArg(ProtocolFormat);
+
+impl From<ProtocolFormatArg> for ProtocolFormat {
+ fn from(arg: ProtocolFormatArg) -> Self {
+ arg.0
+ }
}
-impl ValueEnum for ProtocolFormat {
+impl ValueEnum for ProtocolFormatArg {
fn value_variants<'a>() -> &'a [Self] {
&[
- ProtocolFormat::JsonLegacy,
- ProtocolFormat::PostcardLegacy,
- ProtocolFormat::BidirectionalPostcardPrototype,
+ ProtocolFormatArg(ProtocolFormat::JsonLegacy),
+ ProtocolFormatArg(ProtocolFormat::PostcardLegacy),
+ ProtocolFormatArg(ProtocolFormat::BidirectionalPostcardPrototype),
]
}
fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
- match self {
+ match self.0 {
ProtocolFormat::JsonLegacy => Some(clap::builder::PossibleValue::new("json-legacy")),
ProtocolFormat::PostcardLegacy => {
Some(clap::builder::PossibleValue::new("postcard-legacy"))
@@ -79,12 +83,13 @@ impl ValueEnum for ProtocolFormat {
}
}
}
+
fn from_str(input: &str, _ignore_case: bool) -> Result<Self, String> {
match input {
- "json-legacy" => Ok(ProtocolFormat::JsonLegacy),
- "postcard-legacy" => Ok(ProtocolFormat::PostcardLegacy),
+ "json-legacy" => Ok(ProtocolFormatArg(ProtocolFormat::JsonLegacy)),
+ "postcard-legacy" => Ok(ProtocolFormatArg(ProtocolFormat::PostcardLegacy)),
"bidirectional-postcard-prototype" => {
- Ok(ProtocolFormat::BidirectionalPostcardPrototype)
+ Ok(ProtocolFormatArg(ProtocolFormat::BidirectionalPostcardPrototype))
}
_ => Err(format!("unknown protocol format: {input}")),
}
diff --git a/crates/proc-macro-srv-cli/src/main_loop.rs b/crates/proc-macro-srv-cli/src/main_loop.rs
index 0c651d22b4..b927eea46b 100644
--- a/crates/proc-macro-srv-cli/src/main_loop.rs
+++ b/crates/proc-macro-srv-cli/src/main_loop.rs
@@ -1,6 +1,6 @@
//! The main loop of the proc-macro server.
use proc_macro_api::{
- Codec,
+ Codec, ProtocolFormat,
bidirectional_protocol::msg as bidirectional,
legacy_protocol::msg as legacy,
transport::codec::{json::JsonProtocol, postcard::PostcardProtocol},
@@ -12,7 +12,6 @@ use legacy::Message;
use proc_macro_srv::{EnvSnapshot, SpanId};
-use crate::ProtocolFormat;
struct SpanTrans;
impl legacy::SpanTransformer for SpanTrans {
@@ -32,7 +31,7 @@ impl legacy::SpanTransformer for SpanTrans {
}
}
-pub(crate) fn run(
+pub fn run(
stdin: &mut (dyn BufRead + Send + Sync),
stdout: &mut (dyn Write + Send + Sync),
format: ProtocolFormat,
diff --git a/crates/proc-macro-srv-cli/tests/common/utils.rs b/crates/proc-macro-srv-cli/tests/common/utils.rs
new file mode 100644
index 0000000000..722e92eec7
--- /dev/null
+++ b/crates/proc-macro-srv-cli/tests/common/utils.rs
@@ -0,0 +1,213 @@
+use std::{
+ collections::VecDeque,
+ io::{self, BufRead, Read, Write},
+ sync::{Arc, Condvar, Mutex},
+ thread,
+};
+
+use paths::Utf8PathBuf;
+use proc_macro_api::{
+ legacy_protocol::msg::{FlatTree, Message, Request, Response, SpanDataIndexMap},
+ transport::codec::json::JsonProtocol,
+};
+use span::{Edition, EditionedFileId, FileId, Span, SpanAnchor, SyntaxContext, TextRange};
+use tt::{Delimiter, DelimiterKind, TopSubtreeBuilder};
+
+/// Shared state for an in-memory byte channel.
+#[derive(Default)]
+struct ChannelState {
+ buffer: VecDeque<u8>,
+ closed: bool,
+}
+
+type InMemoryChannel = Arc<(Mutex<ChannelState>, Condvar)>;
+
+/// Writer end of an in-memory channel.
+pub(crate) struct ChannelWriter {
+ state: InMemoryChannel,
+}
+
+impl Write for ChannelWriter {
+ fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
+ let (lock, cvar) = &*self.state;
+ let mut state = lock.lock().unwrap();
+ if state.closed {
+ return Err(io::Error::new(io::ErrorKind::BrokenPipe, "channel closed"));
+ }
+ state.buffer.extend(buf);
+ cvar.notify_all();
+ Ok(buf.len())
+ }
+
+ fn flush(&mut self) -> io::Result<()> {
+ Ok(())
+ }
+}
+
+impl Drop for ChannelWriter {
+ fn drop(&mut self) {
+ let (lock, cvar) = &*self.state;
+ let mut state = lock.lock().unwrap();
+ state.closed = true;
+ cvar.notify_all();
+ }
+}
+
+/// Reader end of an in-memory channel.
+pub(crate) struct ChannelReader {
+ state: InMemoryChannel,
+ internal_buf: Vec<u8>,
+}
+
+impl Read for ChannelReader {
+ fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
+ let (lock, cvar) = &*self.state;
+ let mut state = lock.lock().unwrap();
+
+ while state.buffer.is_empty() && !state.closed {
+ state = cvar.wait(state).unwrap();
+ }
+
+ if state.buffer.is_empty() && state.closed {
+ return Ok(0);
+ }
+
+ let to_read = buf.len().min(state.buffer.len());
+ for (dst, src) in buf.iter_mut().zip(state.buffer.drain(..to_read)) {
+ *dst = src;
+ }
+ Ok(to_read)
+ }
+}
+
+impl BufRead for ChannelReader {
+ fn fill_buf(&mut self) -> io::Result<&[u8]> {
+ let (lock, cvar) = &*self.state;
+ let mut state = lock.lock().unwrap();
+
+ while state.buffer.is_empty() && !state.closed {
+ state = cvar.wait(state).unwrap();
+ }
+
+ self.internal_buf.clear();
+ self.internal_buf.extend(&state.buffer);
+ Ok(&self.internal_buf)
+ }
+
+ fn consume(&mut self, amt: usize) {
+ let (lock, _) = &*self.state;
+ let mut state = lock.lock().unwrap();
+ let to_drain = amt.min(state.buffer.len());
+ drop(state.buffer.drain(..to_drain));
+ }
+}
+
+/// Creates a connected pair of channels for bidirectional communication.
+fn create_channel_pair() -> (ChannelWriter, ChannelReader, ChannelWriter, ChannelReader) {
+ // Channel for client -> server communication
+ let client_to_server = Arc::new((
+ Mutex::new(ChannelState { buffer: VecDeque::new(), closed: false }),
+ Condvar::new(),
+ ));
+ let client_writer = ChannelWriter { state: client_to_server.clone() };
+ let server_reader = ChannelReader { state: client_to_server, internal_buf: Vec::new() };
+
+ // Channel for server -> client communication
+ let server_to_client = Arc::new((
+ Mutex::new(ChannelState { buffer: VecDeque::new(), closed: false }),
+ Condvar::new(),
+ ));
+
+ let server_writer = ChannelWriter { state: server_to_client.clone() };
+ let client_reader = ChannelReader { state: server_to_client, internal_buf: Vec::new() };
+
+ (client_writer, client_reader, server_writer, server_reader)
+}
+
+pub(crate) fn proc_macro_test_dylib_path() -> Utf8PathBuf {
+ let path = proc_macro_test::PROC_MACRO_TEST_LOCATION;
+ if path.is_empty() {
+ panic!("proc-macro-test dylib not available (requires nightly toolchain)");
+ }
+ path.into()
+}
+
+/// Runs a test with the server in a background thread.
+pub(crate) fn with_server<F, R>(test_fn: F) -> R
+where
+ F: FnOnce(&mut dyn Write, &mut dyn BufRead) -> R,
+{
+ let (mut client_writer, mut client_reader, mut server_writer, mut server_reader) =
+ create_channel_pair();
+
+ let server_handle = thread::spawn(move || {
+ proc_macro_srv_cli::main_loop::run(
+ &mut server_reader,
+ &mut server_writer,
+ proc_macro_api::ProtocolFormat::JsonLegacy,
+ )
+ });
+
+ let result = test_fn(&mut client_writer, &mut client_reader);
+
+ // Close the client writer to signal the server to stop
+ drop(client_writer);
+
+ // Wait for server to finish
+ match server_handle.join() {
+ Ok(Ok(())) => {}
+ Ok(Err(e)) => {
+ // IO error from server is expected when client disconnects
+ if matches!(
+ e.kind(),
+ io::ErrorKind::BrokenPipe
+ | io::ErrorKind::UnexpectedEof
+ | io::ErrorKind::InvalidData
+ ) {
+ panic!("Server error: {e}");
+ }
+ }
+ Err(e) => std::panic::resume_unwind(e),
+ }
+
+ result
+}
+
+/// Sends a request and reads the response using JSON protocol.
+pub(crate) fn request(
+ writer: &mut dyn Write,
+ reader: &mut dyn BufRead,
+ request: Request,
+) -> Response {
+ request.write::<JsonProtocol>(writer).expect("failed to write request");
+
+ let mut buf = String::new();
+ Response::read::<JsonProtocol>(reader, &mut buf)
+ .expect("failed to read response")
+ .expect("no response received")
+}
+
+/// Creates a simple empty token tree suitable for testing.
+pub(crate) fn create_empty_token_tree(
+ version: u32,
+ span_data_table: &mut SpanDataIndexMap,
+) -> FlatTree {
+ let anchor = SpanAnchor {
+ file_id: EditionedFileId::new(FileId::from_raw(0), Edition::CURRENT),
+ ast_id: span::ROOT_ERASED_FILE_AST_ID,
+ };
+ let span = Span {
+ range: TextRange::empty(0.into()),
+ anchor,
+ ctx: SyntaxContext::root(Edition::CURRENT),
+ };
+
+ let builder = TopSubtreeBuilder::new(Delimiter {
+ open: span,
+ close: span,
+ kind: DelimiterKind::Invisible,
+ });
+ let tt = builder.build();
+
+ FlatTree::from_subtree(tt.view(), version, span_data_table)
+}
diff --git a/crates/proc-macro-srv-cli/tests/legacy_json.rs b/crates/proc-macro-srv-cli/tests/legacy_json.rs
new file mode 100644
index 0000000000..1fa886219a
--- /dev/null
+++ b/crates/proc-macro-srv-cli/tests/legacy_json.rs
@@ -0,0 +1,224 @@
+//! Integration tests for the proc-macro-srv-cli main loop.
+//!
+//! These tests exercise the full client-server RPC procedure using in-memory
+//! channels without needing to spawn the actual server and client processes.
+
+#![cfg(feature = "sysroot-abi")]
+
+mod common {
+ pub(crate) mod utils;
+}
+
+use common::utils::{create_empty_token_tree, proc_macro_test_dylib_path, request, with_server};
+use expect_test::expect;
+use proc_macro_api::{
+ legacy_protocol::msg::{
+ ExpandMacro, ExpandMacroData, ExpnGlobals, PanicMessage, Request, Response, ServerConfig,
+ SpanDataIndexMap, SpanMode,
+ },
+ version::CURRENT_API_VERSION,
+};
+
+#[test]
+fn test_version_check() {
+ with_server(|writer, reader| {
+ let response = request(writer, reader, Request::ApiVersionCheck {});
+
+ match response {
+ Response::ApiVersionCheck(version) => {
+ assert_eq!(version, CURRENT_API_VERSION);
+ }
+ other => panic!("unexpected response: {other:?}"),
+ }
+ });
+}
+
+#[test]
+fn test_list_macros() {
+ with_server(|writer, reader| {
+ let dylib_path = proc_macro_test_dylib_path();
+ let response = request(writer, reader, Request::ListMacros { dylib_path });
+
+ let Response::ListMacros(Ok(macros)) = response else {
+ panic!("expected successful ListMacros response");
+ };
+
+ let mut macro_list: Vec<_> =
+ macros.iter().map(|(name, kind)| format!("{name} [{kind:?}]")).collect();
+ macro_list.sort();
+ let macro_list_str = macro_list.join("\n");
+
+ expect![[r#"
+ DeriveEmpty [CustomDerive]
+ DeriveError [CustomDerive]
+ DerivePanic [CustomDerive]
+ DeriveReemit [CustomDerive]
+ attr_error [Attr]
+ attr_noop [Attr]
+ attr_panic [Attr]
+ fn_like_clone_tokens [Bang]
+ fn_like_error [Bang]
+ fn_like_mk_idents [Bang]
+ fn_like_mk_literals [Bang]
+ fn_like_noop [Bang]
+ fn_like_panic [Bang]
+ fn_like_span_join [Bang]
+ fn_like_span_line_column [Bang]
+ fn_like_span_ops [Bang]"#]]
+ .assert_eq(&macro_list_str);
+ });
+}
+
+#[test]
+fn test_list_macros_invalid_path() {
+ with_server(|writer, reader| {
+ let response = request(
+ writer,
+ reader,
+ Request::ListMacros { dylib_path: "/nonexistent/path/to/dylib.so".into() },
+ );
+
+ match response {
+ Response::ListMacros(Err(e)) => assert!(
+ e.starts_with("Cannot create expander for /nonexistent/path/to/dylib.so"),
+ "{e}"
+ ),
+ other => panic!("expected error response, got: {other:?}"),
+ }
+ });
+}
+
+#[test]
+fn test_set_config() {
+ with_server(|writer, reader| {
+ let config = ServerConfig { span_mode: SpanMode::Id };
+ let response = request(writer, reader, Request::SetConfig(config));
+
+ match response {
+ Response::SetConfig(returned_config) => {
+ assert_eq!(returned_config.span_mode, SpanMode::Id);
+ }
+ other => panic!("unexpected response: {other:?}"),
+ }
+ });
+}
+
+#[test]
+fn test_set_config_rust_analyzer_mode() {
+ with_server(|writer, reader| {
+ let config = ServerConfig { span_mode: SpanMode::RustAnalyzer };
+ let response = request(writer, reader, Request::SetConfig(config));
+
+ match response {
+ Response::SetConfig(returned_config) => {
+ assert_eq!(returned_config.span_mode, SpanMode::RustAnalyzer);
+ }
+ other => panic!("unexpected response: {other:?}"),
+ }
+ });
+}
+
+#[test]
+fn test_expand_macro_panic() {
+ with_server(|writer, reader| {
+ let dylib_path = proc_macro_test_dylib_path();
+
+ let version_response = request(writer, reader, Request::ApiVersionCheck {});
+ let Response::ApiVersionCheck(version) = version_response else {
+ panic!("expected version check response");
+ };
+
+ let mut span_data_table = SpanDataIndexMap::default();
+ let macro_body = create_empty_token_tree(version, &mut span_data_table);
+
+ let expand_request = Request::ExpandMacro(Box::new(ExpandMacro {
+ lib: dylib_path,
+ env: vec![],
+ current_dir: None,
+ data: ExpandMacroData {
+ macro_body,
+ macro_name: "fn_like_panic".to_owned(),
+ attributes: None,
+ has_global_spans: ExpnGlobals {
+ serialize: version >= 3,
+ def_site: 0,
+ call_site: 0,
+ mixed_site: 0,
+ },
+ span_data_table: vec![],
+ },
+ }));
+
+ let response = request(writer, reader, expand_request);
+
+ match response {
+ Response::ExpandMacro(Err(PanicMessage(msg))) => {
+ assert!(msg.contains("fn_like_panic"), "panic message should mention the macro");
+ }
+ Response::ExpandMacro(Ok(_)) => {
+ panic!("expected panic, but macro succeeded");
+ }
+ other => panic!("unexpected response: {other:?}"),
+ }
+ });
+}
+
+#[test]
+fn test_basic_call_flow() {
+ with_server(|writer, reader| {
+ let dylib_path = proc_macro_test_dylib_path();
+
+ let response1 = request(writer, reader, Request::ApiVersionCheck {});
+ assert!(matches!(response1, Response::ApiVersionCheck(_)));
+
+ let response2 =
+ request(writer, reader, Request::SetConfig(ServerConfig { span_mode: SpanMode::Id }));
+ assert!(matches!(response2, Response::SetConfig(_)));
+
+ let response3 =
+ request(writer, reader, Request::ListMacros { dylib_path: dylib_path.clone() });
+ assert!(matches!(response3, Response::ListMacros(Ok(_))));
+ });
+}
+
+#[test]
+fn test_expand_nonexistent_macro() {
+ with_server(|writer, reader| {
+ let dylib_path = proc_macro_test_dylib_path();
+
+ let version_response = request(writer, reader, Request::ApiVersionCheck {});
+ let Response::ApiVersionCheck(version) = version_response else {
+ panic!("expected version check response");
+ };
+
+ let mut span_data_table = SpanDataIndexMap::default();
+ let macro_body = create_empty_token_tree(version, &mut span_data_table);
+
+ let expand_request = Request::ExpandMacro(Box::new(ExpandMacro {
+ lib: dylib_path,
+ env: vec![],
+ current_dir: None,
+ data: ExpandMacroData {
+ macro_body,
+ macro_name: "NonexistentMacro".to_owned(),
+ attributes: None,
+ has_global_spans: ExpnGlobals {
+ serialize: version >= 3,
+ def_site: 0,
+ call_site: 0,
+ mixed_site: 0,
+ },
+ span_data_table: vec![],
+ },
+ }));
+
+ let response = request(writer, reader, expand_request);
+
+ match response {
+ Response::ExpandMacro(Err(PanicMessage(msg))) => {
+ expect!["proc-macro `NonexistentMacro` is missing"].assert_eq(&msg)
+ }
+ other => panic!("expected error for nonexistent macro, got: {other:?}"),
+ }
+ });
+}