Unnamed repository; edit this file 'description' to name the repository.
-rw-r--r--crates/proc-macro-api/src/bidirectional_protocol.rs8
-rw-r--r--crates/proc-macro-api/src/bidirectional_protocol/msg.rs12
-rw-r--r--crates/proc-macro-srv-cli/tests/bidirectional_postcard.rs225
-rw-r--r--crates/proc-macro-srv-cli/tests/common/utils.rs502
-rw-r--r--crates/proc-macro-srv-cli/tests/legacy_json.rs454
5 files changed, 745 insertions, 456 deletions
diff --git a/crates/proc-macro-api/src/bidirectional_protocol.rs b/crates/proc-macro-api/src/bidirectional_protocol.rs
index b5f43e1d37..a13bff7d7d 100644
--- a/crates/proc-macro-api/src/bidirectional_protocol.rs
+++ b/crates/proc-macro-api/src/bidirectional_protocol.rs
@@ -23,7 +23,6 @@ use crate::{
},
process::ProcMacroServerProcess,
transport::codec::postcard::PostcardProtocol,
- version,
};
pub mod msg;
@@ -159,12 +158,7 @@ pub(crate) fn expand(
macro_name: proc_macro.name.to_string(),
attributes: attr
.map(|subtree| FlatTree::from_subtree(subtree, version, &mut span_data_table)),
- has_global_spans: ExpnGlobals {
- serialize: version >= version::HAS_GLOBAL_SPANS,
- def_site,
- call_site,
- mixed_site,
- },
+ has_global_spans: ExpnGlobals { def_site, call_site, mixed_site },
span_data_table: if process.rust_analyzer_spans() {
serialize_span_data_index_map(&span_data_table)
} else {
diff --git a/crates/proc-macro-api/src/bidirectional_protocol/msg.rs b/crates/proc-macro-api/src/bidirectional_protocol/msg.rs
index c56ed51916..d030498e59 100644
--- a/crates/proc-macro-api/src/bidirectional_protocol/msg.rs
+++ b/crates/proc-macro-api/src/bidirectional_protocol/msg.rs
@@ -84,29 +84,17 @@ pub struct ExpandMacroData {
pub macro_body: FlatTree,
pub macro_name: String,
pub attributes: Option<FlatTree>,
- #[serde(skip_serializing_if = "ExpnGlobals::skip_serializing_if")]
#[serde(default)]
pub has_global_spans: ExpnGlobals,
-
- #[serde(skip_serializing_if = "Vec::is_empty")]
#[serde(default)]
pub span_data_table: Vec<u32>,
}
#[derive(Clone, Copy, Default, Debug, Serialize, Deserialize)]
pub struct ExpnGlobals {
- #[serde(skip_serializing)]
- #[serde(default)]
- pub serialize: bool,
pub def_site: usize,
pub call_site: usize,
pub mixed_site: usize,
}
-impl ExpnGlobals {
- fn skip_serializing_if(&self) -> bool {
- !self.serialize
- }
-}
-
impl Message for BidirectionalMessage {}
diff --git a/crates/proc-macro-srv-cli/tests/bidirectional_postcard.rs b/crates/proc-macro-srv-cli/tests/bidirectional_postcard.rs
new file mode 100644
index 0000000000..33ca1d791d
--- /dev/null
+++ b/crates/proc-macro-srv-cli/tests/bidirectional_postcard.rs
@@ -0,0 +1,225 @@
+#![cfg(feature = "sysroot-abi")]
+
+mod common {
+ pub(crate) mod utils;
+}
+
+use common::utils::{
+ create_empty_token_tree, proc_macro_test_dylib_path, request_bidirectional, with_server,
+};
+use expect_test::expect;
+use proc_macro_api::{
+ ProtocolFormat::BidirectionalPostcardPrototype,
+ bidirectional_protocol::{
+ msg::{ExpandMacro, ExpandMacroData, ExpnGlobals, Request, Response},
+ reject_subrequests,
+ },
+ legacy_protocol::msg::{PanicMessage, ServerConfig, SpanDataIndexMap, SpanMode},
+ version::CURRENT_API_VERSION,
+};
+
+#[test]
+fn test_bidi_version_check_bidirectional() {
+ with_server(BidirectionalPostcardPrototype, |writer, reader| {
+ let response =
+ request_bidirectional(writer, reader, Request::ApiVersionCheck {}, reject_subrequests);
+
+ match response {
+ Response::ApiVersionCheck(version) => {
+ assert_eq!(version, CURRENT_API_VERSION);
+ }
+ other => panic!("unexpected response: {other:?}"),
+ }
+ });
+}
+
+#[test]
+fn test_bidi_list_macros() {
+ with_server(BidirectionalPostcardPrototype, |writer, reader| {
+ let dylib_path = proc_macro_test_dylib_path();
+ let response = request_bidirectional(
+ writer,
+ reader,
+ Request::ListMacros { dylib_path },
+ &reject_subrequests,
+ );
+
+ 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_bidi_list_macros_invalid_path() {
+ with_server(BidirectionalPostcardPrototype, |writer, reader| {
+ let response = request_bidirectional(
+ writer,
+ reader,
+ Request::ListMacros { dylib_path: "/nonexistent/path/to/dylib.so".into() },
+ reject_subrequests,
+ );
+
+ 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_bidi_set_config() {
+ with_server(BidirectionalPostcardPrototype, |writer, reader| {
+ let config = ServerConfig { span_mode: SpanMode::Id };
+ let response =
+ request_bidirectional(writer, reader, Request::SetConfig(config), reject_subrequests);
+
+ match response {
+ Response::SetConfig(returned_config) => {
+ assert_eq!(returned_config.span_mode, SpanMode::Id);
+ }
+ other => panic!("unexpected response: {other:?}"),
+ }
+ });
+}
+
+#[test]
+fn test_bidi_set_config_rust_analyzer_mode() {
+ with_server(BidirectionalPostcardPrototype, |writer, reader| {
+ let config = ServerConfig { span_mode: SpanMode::RustAnalyzer };
+ let response =
+ request_bidirectional(writer, reader, Request::SetConfig(config), reject_subrequests);
+
+ match response {
+ Response::SetConfig(returned_config) => {
+ assert_eq!(returned_config.span_mode, SpanMode::RustAnalyzer);
+ }
+ other => panic!("unexpected response: {other:?}"),
+ }
+ });
+}
+
+#[test]
+fn test_bidi_expand_macro_panic() {
+ with_server(BidirectionalPostcardPrototype, |writer, reader| {
+ let dylib_path = proc_macro_test_dylib_path();
+
+ let mut span_data_table = SpanDataIndexMap::default();
+ let macro_body =
+ common::utils::create_empty_token_tree(CURRENT_API_VERSION, &mut span_data_table);
+
+ let request1 = 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 { def_site: 0, call_site: 0, mixed_site: 0 },
+ span_data_table: vec![],
+ },
+ }));
+
+ let response = request_bidirectional(writer, reader, request1, reject_subrequests);
+
+ match response {
+ Response::ExpandMacro(Err(PanicMessage(msg))) => {
+ assert!(msg.contains("fn_like_panic"), "panic message should mention macro name");
+ }
+ other => panic!("expected panic response, got: {other:?}"),
+ }
+ });
+}
+
+#[test]
+fn test_bidi_basic_call_flow() {
+ with_server(BidirectionalPostcardPrototype, |writer, reader| {
+ let dylib_path = proc_macro_test_dylib_path();
+
+ let response1 =
+ request_bidirectional(writer, reader, Request::ApiVersionCheck {}, reject_subrequests);
+ assert!(matches!(response1, Response::ApiVersionCheck(_)));
+
+ let response2 = request_bidirectional(
+ writer,
+ reader,
+ Request::SetConfig(ServerConfig { span_mode: SpanMode::Id }),
+ reject_subrequests,
+ );
+ assert!(matches!(response2, Response::SetConfig(_)));
+
+ let response3 = request_bidirectional(
+ writer,
+ reader,
+ Request::ListMacros { dylib_path: dylib_path.clone() },
+ reject_subrequests,
+ );
+ assert!(matches!(response3, Response::ListMacros(Ok(_))));
+ });
+}
+
+#[test]
+fn test_bidi_expand_nonexistent_macro() {
+ with_server(BidirectionalPostcardPrototype, |writer, reader| {
+ let dylib_path = proc_macro_test_dylib_path();
+
+ let version_response =
+ request_bidirectional(writer, reader, Request::ApiVersionCheck {}, reject_subrequests);
+ 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 { def_site: 0, call_site: 0, mixed_site: 0 },
+ span_data_table: vec![],
+ },
+ }));
+
+ let response = request_bidirectional(writer, reader, expand_request, reject_subrequests);
+
+ 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:?}"),
+ }
+ });
+}
diff --git a/crates/proc-macro-srv-cli/tests/common/utils.rs b/crates/proc-macro-srv-cli/tests/common/utils.rs
index 722e92eec7..85c394734b 100644
--- a/crates/proc-macro-srv-cli/tests/common/utils.rs
+++ b/crates/proc-macro-srv-cli/tests/common/utils.rs
@@ -1,213 +1,289 @@
-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)
-}
+use std::{
+ collections::VecDeque,
+ io::{self, BufRead, Read, Write},
+ sync::{Arc, Condvar, Mutex},
+ thread,
+};
+
+use paths::Utf8PathBuf;
+use proc_macro_api::{
+ ServerError,
+ bidirectional_protocol::msg::{
+ BidirectionalMessage, Request as BiRequest, Response as BiResponse, SubRequest, SubResponse,
+ },
+ legacy_protocol::msg::{FlatTree, Message, Request, Response, SpanDataIndexMap},
+ transport::codec::{json::JsonProtocol, postcard::PostcardProtocol},
+};
+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()
+}
+
+/// 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)
+}
+
+pub(crate) fn with_server<F, R>(format: proc_macro_api::ProtocolFormat, 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, format)
+ });
+
+ let result = test_fn(&mut client_writer, &mut client_reader);
+
+ drop(client_writer);
+
+ match server_handle.join() {
+ Ok(Ok(())) => {}
+ Ok(Err(e)) => {
+ 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
+}
+
+trait TestProtocol {
+ type Request;
+ type Response;
+
+ fn request(&self, writer: &mut dyn Write, req: Self::Request);
+ fn receive(&self, reader: &mut dyn BufRead, writer: &mut dyn Write) -> Self::Response;
+}
+
+#[allow(dead_code)]
+struct JsonLegacy;
+
+impl TestProtocol for JsonLegacy {
+ type Request = Request;
+ type Response = Response;
+
+ fn request(&self, writer: &mut dyn Write, req: Request) {
+ req.write::<JsonProtocol>(writer).expect("failed to write request");
+ }
+
+ fn receive(&self, reader: &mut dyn BufRead, _writer: &mut dyn Write) -> Response {
+ let mut buf = String::new();
+ Response::read::<JsonProtocol>(reader, &mut buf)
+ .expect("failed to read response")
+ .expect("no response received")
+ }
+}
+
+#[allow(dead_code)]
+struct PostcardBidirectional<F>
+where
+ F: Fn(SubRequest) -> Result<SubResponse, ServerError>,
+{
+ callback: F,
+}
+
+impl<F> TestProtocol for PostcardBidirectional<F>
+where
+ F: Fn(SubRequest) -> Result<SubResponse, ServerError>,
+{
+ type Request = BiRequest;
+ type Response = BiResponse;
+
+ fn request(&self, writer: &mut dyn Write, req: BiRequest) {
+ let msg = BidirectionalMessage::Request(req);
+ msg.write::<PostcardProtocol>(writer).expect("failed to write request");
+ }
+
+ fn receive(&self, reader: &mut dyn BufRead, writer: &mut dyn Write) -> BiResponse {
+ let mut buf = Vec::new();
+
+ loop {
+ let msg = BidirectionalMessage::read::<PostcardProtocol>(reader, &mut buf)
+ .expect("failed to read message")
+ .expect("no message received");
+
+ match msg {
+ BidirectionalMessage::Response(resp) => return resp,
+ BidirectionalMessage::SubRequest(sr) => {
+ let reply = (self.callback)(sr).expect("subrequest callback failed");
+ let msg = BidirectionalMessage::SubResponse(reply);
+ msg.write::<PostcardProtocol>(writer).expect("failed to write subresponse");
+ }
+ other => panic!("unexpected message: {other:?}"),
+ }
+ }
+ }
+}
+
+#[allow(dead_code)]
+pub(crate) fn request_legacy(
+ writer: &mut dyn Write,
+ reader: &mut dyn BufRead,
+ request: Request,
+) -> Response {
+ let protocol = JsonLegacy;
+ protocol.request(writer, request);
+ protocol.receive(reader, writer)
+}
+
+#[allow(dead_code)]
+pub(crate) fn request_bidirectional<F>(
+ writer: &mut dyn Write,
+ reader: &mut dyn BufRead,
+ request: BiRequest,
+ callback: F,
+) -> BiResponse
+where
+ F: Fn(SubRequest) -> Result<SubResponse, ServerError>,
+{
+ let protocol = PostcardBidirectional { callback };
+ protocol.request(writer, request);
+ protocol.receive(reader, writer)
+}
diff --git a/crates/proc-macro-srv-cli/tests/legacy_json.rs b/crates/proc-macro-srv-cli/tests/legacy_json.rs
index 1fa886219a..c0dbfd1679 100644
--- a/crates/proc-macro-srv-cli/tests/legacy_json.rs
+++ b/crates/proc-macro-srv-cli/tests/legacy_json.rs
@@ -1,224 +1,230 @@
-//! 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:?}"),
- }
- });
-}
+//! 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_legacy, with_server,
+};
+use expect_test::expect;
+use proc_macro_api::{
+ ProtocolFormat::JsonLegacy,
+ legacy_protocol::msg::{
+ ExpandMacro, ExpandMacroData, ExpnGlobals, PanicMessage, Request, Response, ServerConfig,
+ SpanDataIndexMap, SpanMode,
+ },
+ version::CURRENT_API_VERSION,
+};
+
+#[test]
+fn test_version_check() {
+ with_server(JsonLegacy, |writer, reader| {
+ let response = request_legacy(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(JsonLegacy, |writer, reader| {
+ let dylib_path = proc_macro_test_dylib_path();
+ let response = request_legacy(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(JsonLegacy, |writer, reader| {
+ let response = request_legacy(
+ 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(JsonLegacy, |writer, reader| {
+ let config = ServerConfig { span_mode: SpanMode::Id };
+ let response = request_legacy(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(JsonLegacy, |writer, reader| {
+ let config = ServerConfig { span_mode: SpanMode::RustAnalyzer };
+ let response = request_legacy(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(JsonLegacy, |writer, reader| {
+ let dylib_path = proc_macro_test_dylib_path();
+
+ let version_response = request_legacy(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_legacy(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(JsonLegacy, |writer, reader| {
+ let dylib_path = proc_macro_test_dylib_path();
+
+ let response1 = request_legacy(writer, reader, Request::ApiVersionCheck {});
+ assert!(matches!(response1, Response::ApiVersionCheck(_)));
+
+ let response2 = request_legacy(
+ writer,
+ reader,
+ Request::SetConfig(ServerConfig { span_mode: SpanMode::Id }),
+ );
+ assert!(matches!(response2, Response::SetConfig(_)));
+
+ let response3 =
+ request_legacy(writer, reader, Request::ListMacros { dylib_path: dylib_path.clone() });
+ assert!(matches!(response3, Response::ListMacros(Ok(_))));
+ });
+}
+
+#[test]
+fn test_expand_nonexistent_macro() {
+ with_server(JsonLegacy, |writer, reader| {
+ let dylib_path = proc_macro_test_dylib_path();
+
+ let version_response = request_legacy(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_legacy(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:?}"),
+ }
+ });
+}