use super::*;
use crate::code::text::{Change, Position, Range};
fn omni_on_path() {
let omni = std::env::var_os("WSB_OMNI").expect("WSB_OMNI must name an omni binary for this test");
let dir = Path::new(&omni).parent().expect("a directory").to_owned();
let path = std::env::var_os("PATH").unwrap_or_default();
let mut dirs = vec![dir];
dirs.extend(std::env::split_paths(&path));
std::env::set_var("PATH", std::env::join_paths(dirs).expect("a PATH"));
}
async fn next(events: &mut broadcast::Receiver<CodeEvent>, what: &str, pred: impl Fn(&CodeEvent) -> bool) -> CodeEvent {
let wait = async {
loop {
match events.recv().await {
Ok(e) if pred(&e) => return e,
Ok(_) => {}
Err(e) => panic!("the events ended waiting for {what}: {e}"),
}
}
};
tokio::time::timeout(Duration::from_secs(180), wait).await.unwrap_or_else(|_| panic!("no {what}"))
}
fn git(dir: &Path, args: &[&str]) {
let o = wsb::git_backend::git_command().arg("-C").arg(dir).args(args).output().expect("git runs");
assert!(o.status.success(), "git {args:?}: {}", String::from_utf8_lossy(&o.stderr));
}
fn server_pids(dir: &Path) -> Vec<u32> {
let mut pids = Vec::new();
for entry in std::fs::read_dir("/proc").expect("/proc") {
let Ok(entry) = entry else { continue };
let Ok(pid) = entry.file_name().to_string_lossy().parse::<u32>() else { continue };
let cmd = std::fs::read(entry.path().join("cmdline")).unwrap_or_default();
let cwd = std::fs::read_link(entry.path().join("cwd")).ok();
if String::from_utf8_lossy(&cmd).contains("lsp\0--daemon") && cwd.as_deref() == Some(dir) {
pids.push(pid);
}
}
pids
}
fn server_pid(dir: &Path) -> u32 {
match server_pids(dir)[..] {
[pid] => pid,
ref pids => panic!("{} omni lsp processes serve {}: {pids:?}", pids.len(), dir.display()),
}
}
struct StopsOmni(std::path::PathBuf, std::path::PathBuf);
impl Drop for StopsOmni {
fn drop(&mut self) {
let servers = server_pids(&self.1);
wsb::ops::process::signal(&servers, "KILL");
let deadline = std::time::Instant::now() + Duration::from_secs(10);
while servers.iter().any(|p| wsb::ops::process::is_alive(*p)) && std::time::Instant::now() < deadline {
std::thread::sleep(Duration::from_millis(20));
}
let omni = std::env::var_os("WSB_OMNI").expect("WSB_OMNI");
let _ = std::process::Command::new(omni).env("OMNI_STATE_DIR", &self.0).args(["daemon", "stop"]).output();
}
}
const UTIL: &str = "def helper(x):\n \"\"\"Double it.\"\"\"\n return x * 2\n";
const APP: &str = "from util import helper\n\nhelper(1)\n";
#[tokio::test(flavor = "multi_thread")]
#[ignore = "needs WSB_OMNI: the path of an omni binary"]
async fn test20093_a_page_is_answered_about_its_own_text_through_a_server_that_ends() {
omni_on_path();
let state = tempfile::tempdir().expect("omni's state");
std::env::set_var("OMNI_STATE_DIR", state.path());
let ws = tempfile::tempdir().expect("a worktree");
let root = wsb::platform::canonicalize(ws.path()).expect("resolves");
let _stops = StopsOmni(state.path().to_owned(), root.clone());
git(&root, &["init", "-q"]);
std::fs::write(root.join("util.py"), UTIL).expect("a file");
std::fs::write(root.join("app.py"), APP).expect("a file");
std::fs::write(root.join("notes.txt"), "nothing to analyze\n").expect("a file");
let intel = CodeIntel::new(&root).expect("code intelligence");
let mut events = intel.subscribe();
match intel.open("a", "app.py", 1, APP).await.expect("opened") {
Opened::Yes { root: r, language, features } => {
assert_eq!((r.as_str(), language.as_str()), (".", "python"));
assert!(features.iter().any(|f| f == "definition"), "{features:?}");
}
Opened::No { reason } => panic!("not served: {reason}"),
}
next(&mut events, "the server ready", |e| matches!(e, CodeEvent::Status { status: RootStatus { state: State::Ready { .. }, .. }, .. })).await;
let call = json!({ "position": { "line": 2, "character": 1 } });
let def = intel.ask("a", "app.py", 1, "definition", call.clone()).await.expect("an answer");
assert_eq!(def[0]["uri"], json!({ "path": "util.py" }), "{def}");
let broken = format!("{APP}def broken(:\n");
let edit = Change {
range: Some(Range { start: Position { line: 3, character: 0 }, end: Position { line: 3, character: 0 } }),
text: "def broken(:\n".into(),
};
intel.change("a", "app.py", 1, 2, &[edit]).await.expect("an edit");
let want = text::digest(&broken);
let event = next(&mut events, "diagnostics of the edit", |e| matches!(e, CodeEvent::Diagnostics { digest, .. } if *digest == want)).await;
let CodeEvent::Diagnostics { diagnostics, .. } = event else { unreachable!() };
assert!(!diagnostics.is_empty(), "a syntax error has no diagnostic");
intel.open("b", "app.py", 1, APP).await.expect("opened by b");
intel.ask("b", "app.py", 1, "definition", call.clone()).await.expect("b's answer");
let original = text::digest(APP);
next(&mut events, "diagnostics of b's text", |e| matches!(e, CodeEvent::Diagnostics { digest, .. } if *digest == original)).await;
assert!(
matches!(intel.ask("a", "app.py", 1, "definition", call.clone()).await, Err(AskError::Stale(_))),
"a question about text the page no longer has was answered"
);
let blank = Change {
range: Some(Range { start: Position { line: 1, character: 0 }, end: Position { line: 1, character: 0 } }),
text: "\n".into(),
};
intel.change("b", "app.py", 1, 2, &[blank]).await.expect("b's edit");
let moved = json!({ "position": { "line": 3, "character": 1 } });
intel.ask("b", "app.py", 2, "definition", moved.clone()).await.expect("b's answer about its edit");
wsb::ops::process::signal(&[server_pid(&root)], "KILL");
next(&mut events, "the crash", |e| matches!(e, CodeEvent::Status { status: RootStatus { state: State::Crashed { .. }, .. }, .. })).await;
assert!(matches!(intel.ask("b", "app.py", 2, "definition", moved.clone()).await, Err(AskError::Unavailable(_))));
let refused = intel.open("c", "util.py", 1, UTIL).await.expect_err("a file opened while the server is down");
assert!(refused.contains("start it again"), "{refused}");
intel.restart(".").await.expect("started again");
next(&mut events, "the server ready again", |e| matches!(e, CodeEvent::Status { status: RootStatus { state: State::Ready { .. }, .. }, .. })).await;
let def = intel.ask("b", "app.py", 2, "definition", moved).await.expect("an answer after the restart");
assert_eq!(def[0]["uri"], json!({ "path": "util.py" }), "the restarted server does not hold the page's text: {def}");
match intel.open("a", "notes.txt", 1, "x").await.expect("an answer") {
Opened::No { reason } => assert!(reason.contains("does not analyze"), "{reason}"),
other => panic!("served: {other:?}"),
}
}