use std::io::{Read, Write as _};
use std::process::{Command, Output, Stdio};
fn frame(body: &str) -> Vec<u8> {
format!("Content-Length: {}\r\n\r\n{body}", body.len()).into_bytes()
}
const INITIALIZE: &str = r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"processId":null,"rootUri":null,"capabilities":{}}}"#;
const REST: &[&str] = &[
r#"{"jsonrpc":"2.0","method":"initialized","params":{}}"#,
r#"{"jsonrpc":"2.0","id":2,"method":"shutdown"}"#,
r#"{"jsonrpc":"2.0","method":"exit"}"#,
];
fn read_frame(reader: &mut impl Read) -> Vec<u8> {
let mut bytes = Vec::new();
let mut byte = [0_u8; 1];
while !bytes.ends_with(b"\r\n\r\n") {
reader.read_exact(&mut byte).expect("a header byte");
bytes.push(byte[0]);
}
let header = String::from_utf8_lossy(&bytes).into_owned();
let length: usize = header
.lines()
.find_map(|line| line.strip_prefix("Content-Length: "))
.unwrap_or_else(|| panic!("a header without Content-Length: {header:?}"))
.parse()
.expect("a numeric Content-Length");
let mut body = vec![0_u8; length];
reader.read_exact(&mut body).expect("the body");
bytes.extend(body);
bytes
}
fn serve(binary: &str, args: &[&str], env: &[(&str, &str)]) -> Output {
let mut child = Command::new(binary)
.args(args)
.env_clear()
.envs(env.iter().copied())
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("start the server");
let mut stdin = child.stdin.take().expect("stdin");
let mut stdout = child.stdout.take().expect("stdout");
stdin
.write_all(&frame(INITIALIZE))
.expect("write initialize");
let mut written = Vec::new();
loop {
let message = read_frame(&mut stdout);
written.extend_from_slice(&message);
if messages(&message)[0]["id"] == 1 {
break;
}
}
for body in REST {
stdin.write_all(&frame(body)).expect("write the session");
}
drop(stdin);
stdout.read_to_end(&mut written).expect("read stdout");
let mut output = child.wait_with_output().expect("wait for the server");
output.stdout = written;
output
}
fn messages(stdout: &[u8]) -> Vec<serde_json::Value> {
let mut rest = stdout;
let mut found = Vec::new();
while !rest.is_empty() {
let end = rest
.windows(4)
.position(|window| window == b"\r\n\r\n")
.unwrap_or_else(|| {
panic!(
"unframed bytes on stdout: {:?}",
String::from_utf8_lossy(rest)
)
});
let header = std::str::from_utf8(&rest[..end]).expect("an ASCII header");
let length: usize = header
.lines()
.find_map(|line| line.strip_prefix("Content-Length: "))
.unwrap_or_else(|| panic!("a header without Content-Length: {header:?}"))
.parse()
.expect("a numeric Content-Length");
let body = &rest[end + 4..end + 4 + length];
found.push(serde_json::from_slice(body).expect("a JSON body"));
rest = &rest[end + 4 + length..];
}
found
}
fn assert_only_the_protocol(output: &Output) {
assert!(
output.stderr.is_empty(),
"editors parse stderr, so a working session must write nothing to it; got {:?}",
String::from_utf8_lossy(&output.stderr)
);
let messages = messages(&output.stdout);
for message in &messages {
assert_eq!(
message["jsonrpc"], "2.0",
"not a JSON-RPC message: {message}"
);
}
let initialize = messages
.iter()
.find(|message| message["id"] == 1)
.expect("a reply to initialize");
assert!(
initialize["result"]["serverInfo"]["version"]
.as_str()
.is_some_and(|version| version.starts_with(env!("CARGO_PKG_VERSION"))),
"initialize reports the release: {initialize}"
);
assert!(
messages.iter().any(|message| message["id"] == 2),
"a reply to shutdown"
);
assert_eq!(output.status.code(), Some(0), "a clean exit");
}
#[test]
fn surreal_lsp_serves_only_the_protocol() {
let output = serve(env!("CARGO_BIN_EXE_surreal-lsp"), &[], &[]);
assert_only_the_protocol(&output);
}
#[test]
fn surreal_lsp_serves_only_the_protocol_with_stdio() {
let output = serve(env!("CARGO_BIN_EXE_surreal-lsp"), &["--stdio"], &[]);
assert_only_the_protocol(&output);
}
#[test]
fn the_old_name_serves_only_the_protocol() {
let binary = env!("CARGO_BIN_EXE_surrealql-language-server");
assert_only_the_protocol(&serve(binary, &[], &[]));
assert_only_the_protocol(&serve(binary, &["--stdio"], &[]));
}
#[test]
fn the_stdio_command_serves_only_the_protocol() {
for binary in [
env!("CARGO_BIN_EXE_surreal-lsp"),
env!("CARGO_BIN_EXE_surrealql-language-server"),
] {
assert_only_the_protocol(&serve(binary, &["stdio"], &[]));
assert_only_the_protocol(&serve(binary, &["stdio", "--stdio"], &[]));
}
}
const LAUNCHER_ENV: [(&str, &str); 2] = [
("SURREAL_INVOKED_AS", "surreal lsp"),
("SURREAL_CLI_LAUNCHER", "/usr/local/bin/surreal"),
];
#[test]
fn started_by_the_launcher_it_serves_only_the_protocol() {
let binary = env!("CARGO_BIN_EXE_surreal-lsp");
assert_only_the_protocol(&serve(binary, &["stdio"], &LAUNCHER_ENV));
assert_only_the_protocol(&serve(binary, &["--stdio"], &LAUNCHER_ENV));
}
fn run_bare(binary: &str, env: &[(&str, &str)]) -> Output {
Command::new(binary)
.env_clear()
.envs(env.iter().copied())
.stdin(Stdio::null())
.output()
.expect("run the binary")
}
#[test]
fn started_by_the_launcher_with_no_command_it_prints_help() {
let output = run_bare(env!("CARGO_BIN_EXE_surreal-lsp"), &LAUNCHER_ENV);
assert_eq!(output.status.code(), Some(2));
assert!(output.stdout.is_empty(), "stdout: {:?}", output.stdout);
let help = String::from_utf8(output.stderr).expect("utf-8 help");
assert!(help.contains("Usage: surreal lsp <COMMAND>\n"), "{help}");
assert!(help.contains("stdio"), "{help}");
assert!(help.contains("check"), "{help}");
}
#[test]
fn an_empty_launcher_variable_keeps_the_legacy_flow() {
let output = serve(
env!("CARGO_BIN_EXE_surreal-lsp"),
&[],
&[("SURREAL_CLI_LAUNCHER", "")],
);
assert_only_the_protocol(&output);
}
#[test]
fn help_names_the_launcher_invocation_when_it_is_given_one() {
let help = |binary: &str, invoked_as: Option<&str>| {
let mut command = Command::new(binary);
command
.args(["check", "--help"])
.env_remove("SURREAL_INVOKED_AS");
if let Some(name) = invoked_as {
command.env("SURREAL_INVOKED_AS", name);
}
let output = command.output().expect("the binary should run");
assert!(output.status.success());
String::from_utf8(output.stdout).expect("utf-8 help")
};
let lsp = env!("CARGO_BIN_EXE_surreal-lsp");
let old = env!("CARGO_BIN_EXE_surrealql-language-server");
assert!(help(lsp, Some("surreal lsp")).contains("Usage: surreal lsp check [OPTIONS]"));
assert!(help(lsp, None).contains("Usage: surreal-lsp check [OPTIONS]"));
assert!(help(old, None).contains("Usage: surrealql-language-server check [OPTIONS]"));
assert!(help(lsp, Some("surreal\x1b[2Jlsp")).contains("Usage: surreal-lsp check [OPTIONS]"));
}
#[test]
fn version_names_the_executable_and_the_release() {
for (binary, name) in [
(env!("CARGO_BIN_EXE_surreal-lsp"), "surreal-lsp"),
(
env!("CARGO_BIN_EXE_surrealql-language-server"),
"surrealql-language-server",
),
] {
let output = Command::new(binary)
.arg("--version")
.env_remove("SURREAL_INVOKED_AS")
.output()
.expect("spawn");
assert_eq!(output.status.code(), Some(0));
let line = String::from_utf8(output.stdout).expect("utf-8");
let expected = format!("{name} {} (", env!("CARGO_PKG_VERSION"));
assert!(line.starts_with(&expected), "got {line:?}");
}
}