#![allow(dead_code)]
use std::io::{BufRead, BufReader, Write};
use std::path::{Path, PathBuf};
use std::process::{Child, ChildStdin, ChildStdout, Command, Stdio};
use std::sync::mpsc::{self, Receiver};
use std::time::Duration;
pub fn binary_path() -> PathBuf {
PathBuf::from(env!("CARGO_BIN_EXE_reliary"))
}
pub struct Fixture {
pub dir: tempfile::TempDir,
}
impl Fixture {
pub fn new() -> Fixture {
let dir = tempfile::tempdir().expect("tempdir");
let root = dir.path();
std::fs::create_dir_all(root.join("src")).unwrap();
std::fs::write(
root.join("src/lib.rs"),
r#"//! Fixture crate.
/// Alpha does a thing.
pub fn alpha() -> i32 {
beta() + 1
}
"#,
)
.unwrap();
std::fs::write(
root.join("src/other.rs"),
r#"/// Beta does another thing.
pub fn beta() -> i32 {
41
}
"#,
)
.unwrap();
let ok = Command::new("git")
.args(["init", "--quiet"])
.current_dir(root)
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false);
assert!(
ok,
"git init failed — the e2e tests require git on PATH (install git, or skip with --skip e2e_)"
);
Fixture { dir }
}
pub fn trait_impls() -> Fixture {
let dir = tempfile::tempdir().expect("tempdir");
let root = dir.path();
std::fs::create_dir_all(root.join("src")).unwrap();
std::fs::write(
root.join("src/models.rs"),
r#"#[derive(Debug, Clone, Default)]
pub struct Derived {
pub id: i32,
}
pub struct Manual {
pub id: i32,
}
impl Default for Manual {
fn default() -> Self {
Self { id: 0 }
}
}
#[derive(Debug, Clone)]
pub struct NotDefault {
pub id: i32,
}
"#,
)
.unwrap();
let ok = Command::new("git")
.args(["init", "--quiet"])
.current_dir(root)
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false);
if ok {
for a in [["add", "-A"].as_slice(), ["-c", "user.email=t@t", "-c", "user.name=t", "commit", "-qm", "fixture"].as_slice()] {
let _ = Command::new("git").args(a).current_dir(root)
.stdout(Stdio::null()).stderr(Stdio::null()).status();
}
}
Fixture { dir }
}
pub fn many_dead_functions(n: usize) -> Fixture {
let dir = tempfile::tempdir().expect("tempdir");
let root = dir.path();
std::fs::create_dir_all(root.join("src")).unwrap();
let mut body = String::from("pub fn live_entry() -> i32 {\n 1\n}\n\n");
for i in 0..n {
body.push_str(&format!("fn unused_helper_{i}() -> i32 {{\n {i}\n}}\n\n"));
}
std::fs::write(root.join("src/lib.rs"), body).unwrap();
let ok = Command::new("git")
.args(["init", "--quiet"])
.current_dir(root)
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false);
if ok {
for a in [["add", "-A"].as_slice(),
["-c", "user.email=t@t", "-c", "user.name=t", "commit", "-qm", "fixture"].as_slice()] {
let _ = Command::new("git").args(a).current_dir(root)
.stdout(Stdio::null()).stderr(Stdio::null()).status();
}
}
Fixture { dir }
}
pub fn mixed_dead_functions(priv_n: usize, _unused: usize) -> Fixture {
Self::mixed_dead_impl(priv_n)
}
fn mixed_dead_impl(pub_n: usize) -> Fixture {
let dir = tempfile::tempdir().expect("tempdir");
let root = dir.path();
std::fs::create_dir_all(root.join("src")).unwrap();
let mut body = String::from("pub fn live_entry() -> i32 {\n 1\n}\n\n");
for i in 0..pub_n {
body.push_str(&format!("pub fn pub_unused_{i}() -> i32 {{\n {i}\n}}\n\n"));
}
for i in 0..12 {
body.push_str(&format!("fn priv_unused_{i}() -> i32 {{\n {i}\n}}\n\n"));
}
std::fs::write(root.join("src/lib.rs"), body).unwrap();
let ok = Command::new("git").args(["init", "--quiet"]).current_dir(root)
.stdout(Stdio::null()).stderr(Stdio::null())
.status().map(|s| s.success()).unwrap_or(false);
if ok {
for a in [["add", "-A"].as_slice(),
["-c","user.email=t@t","-c","user.name=t","commit","-qm","fixture"].as_slice()] {
let _ = Command::new("git").args(a).current_dir(root)
.stdout(Stdio::null()).stderr(Stdio::null()).status();
}
}
Fixture { dir }
}
pub fn trust(&self) {
let mut cmd = Command::new(binary_path());
cmd.args(["trust", "."])
.current_dir(self.path())
.env("NO_RELIARY_WATCHER", "1");
let no_envs: &[(&str, &str)] = &[];
apply_xdg_isolation(&mut cmd, self.path(), no_envs);
let out = cmd.output().expect("run trust");
assert!(
out.status.success(),
"trust failed: {}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
assert!(
self.path().join(".reliary/index.sqlite").exists(),
"trust did not create an index"
);
}
pub fn path(&self) -> &Path {
self.dir.path()
}
}
pub fn apply_xdg_isolation(cmd: &mut Command, cwd: &Path, envs: &[(&str, &str)]) {
let has = |k: &str| envs.iter().any(|(ek, _)| *ek == k);
if !has("XDG_CONFIG_HOME") {
cmd.env("XDG_CONFIG_HOME", cwd.join(".config"));
}
if !has("XDG_DATA_HOME") {
cmd.env("XDG_DATA_HOME", cwd.join(".local/share"));
}
}
pub fn run_cli(args: &[&str], cwd: &Path, envs: &[(&str, &str)]) -> std::process::Output {
let mut cmd = Command::new(binary_path());
cmd.args(args).current_dir(cwd);
cmd.env("NO_RELIARY_WATCHER", "1");
apply_xdg_isolation(&mut cmd, cwd, envs);
for (k, v) in envs {
cmd.env(k, v);
}
cmd.output().expect("spawn reliary")
}
pub fn run_cli_stdin(args: &[&str], cwd: &Path, envs: &[(&str, &str)], input: &str) -> std::process::Output {
use std::io::Write;
let mut cmd = Command::new(binary_path());
cmd.args(args)
.current_dir(cwd)
.env("NO_RELIARY_WATCHER", "1")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
apply_xdg_isolation(&mut cmd, cwd, envs);
for (k, v) in envs {
cmd.env(k, v);
}
let mut child = cmd.spawn().expect("spawn reliary");
child
.stdin
.as_mut()
.expect("stdin piped")
.write_all(input.as_bytes())
.expect("write stdin");
child.wait_with_output().expect("wait reliary")
}
pub struct Mcp {
child: Child,
stdin: ChildStdin,
responses: Receiver<serde_json::Value>,
_stdout_reader: std::thread::JoinHandle<()>,
next_id: i64,
}
impl Mcp {
pub fn start(cwd: &Path) -> Mcp {
let mut cmd = Command::new(binary_path());
cmd.arg("mcp")
.current_dir(cwd)
.env("NO_RELIARY_WATCHER", "1")
.env("RELIARY_RESULT_CACHE", "0")
.env("RELIARY_NO_TRUNCATE", "1")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null());
let no_envs: &[(&str, &str)] = &[];
apply_xdg_isolation(&mut cmd, cwd, no_envs);
let mut child = cmd.spawn().expect("spawn reliary mcp");
let stdin = child.stdin.take().expect("child stdin");
let stdout: ChildStdout = child.stdout.take().expect("child stdout");
let (tx, rx) = mpsc::channel();
let reader = std::thread::spawn(move || {
let mut lines = BufReader::new(stdout).lines();
while let Some(Ok(line)) = lines.next() {
let line = line.trim();
if line.is_empty() {
continue;
}
match serde_json::from_str::<serde_json::Value>(line) {
Ok(v) => {
if tx.send(v).is_err() {
break;
}
}
Err(_) => {
let _ = tx.send(serde_json::json!({ "__non_json__": line }));
}
}
}
});
Mcp { child, stdin, responses: rx, _stdout_reader: reader, next_id: 1 }
}
pub fn request(&mut self, method: &str, params: serde_json::Value) -> serde_json::Value {
let id = self.next_id;
self.next_id += 1;
self.send_raw(&serde_json::json!({
"jsonrpc": "2.0",
"id": id,
"method": method,
"params": params,
}));
self.read_response(Duration::from_secs(30))
}
pub fn send_raw(&mut self, msg: &serde_json::Value) {
let line = serde_json::to_string(msg).unwrap();
self.send_line(&line);
}
pub fn send_line(&mut self, line: &str) {
writeln!(self.stdin, "{}", line).expect("write stdin");
self.stdin.flush().expect("flush stdin");
}
pub fn read_response(&mut self, timeout: Duration) -> serde_json::Value {
self.responses
.recv_timeout(timeout)
.unwrap_or_else(|_| panic!("timed out after {:?} waiting for MCP response", timeout))
}
pub fn try_read_response(&mut self, timeout: Duration) -> Option<serde_json::Value> {
self.responses.recv_timeout(timeout).ok()
}
pub fn initialize(&mut self) -> serde_json::Value {
self.request(
"initialize",
serde_json::json!({ "protocolVersion": "2024-11-05" }),
)
}
pub fn list_tools(&mut self) -> Vec<serde_json::Value> {
let resp = self.request("tools/list", serde_json::json!({}));
resp["result"]["tools"]
.as_array()
.unwrap_or_else(|| panic!("tools/list did not return an array: {}", resp))
.clone()
}
pub fn call_tool(&mut self, name: &str, args: serde_json::Value) -> serde_json::Value {
self.request(
"tools/call",
serde_json::json!({ "name": name, "arguments": args }),
)
}
pub fn call_tool_text(&mut self, name: &str, args: serde_json::Value) -> String {
let resp = self.call_tool(name, args);
assert!(
resp.get("error").is_none(),
"tool {} returned an error: {}",
name,
resp
);
resp["result"]["content"]
.as_array()
.unwrap_or_else(|| panic!("tool {} returned no content array: {}", name, resp))
.iter()
.filter_map(|c| c.get("text").and_then(|t| t.as_str()))
.collect::<Vec<_>>()
.join("\n")
}
pub fn is_alive(&mut self) -> bool {
matches!(self.child.try_wait(), Ok(None))
}
}
impl Drop for Mcp {
fn drop(&mut self) {
let _ = self.child.kill();
let _ = self.child.wait();
}
}