pub mod proto {
connectrpc::include_generated!();
}
use std::{
path::Path,
process::{Command, Output},
};
use tempfile::TempDir;
struct Instance(TempDir);
impl Drop for Instance {
fn drop(&mut self) {
command(self.0.path(), &["down"]);
}
}
#[tokio::test]
async fn background_instance_serves_ui_and_rpc_and_stops() {
let instance = Instance(tempfile::tempdir().unwrap());
let dir = instance.0.path();
let up = command(dir, &["up", "--port", "0", "--no-open"]);
assert!(
up.status.success(),
"{}",
String::from_utf8_lossy(&up.stderr)
);
let state = discovery(dir);
let url = state["ui_url"].as_str().unwrap().to_owned();
let client = reqwest::Client::builder().no_proxy().build().unwrap();
let grpc = proto::lgtm::v1::GreeterServiceClient::new(
connectrpc::client::HttpClient::plaintext_http2_only(),
connectrpc::client::ClientConfig::new(url.parse().unwrap())
.with_protocol(connectrpc::Protocol::Grpc),
);
let reply = grpc
.say_hello(proto::lgtm::v1::SayHelloRequest {
name: "gRPC".into(),
..Default::default()
})
.await
.unwrap();
assert_eq!(reply.into_owned().message, "Hello, gRPC!");
let page = client.get(&url).send().await.unwrap();
assert_eq!(page.status(), 200);
assert!(page.text().await.unwrap().contains("LGTM Hello World"));
for prefix in ["", "/rpc"] {
let response = client
.post(format!("{url}{prefix}/lgtm.v1.GreeterService/SayHello"))
.json(&serde_json::json!({"name":"Package"}))
.send()
.await
.unwrap();
assert_eq!(response.status(), 200);
assert_eq!(
response.json::<serde_json::Value>().await.unwrap()["message"],
"Hello, Package!"
);
}
assert_eq!(
client
.get(format!("{url}/missing.js"))
.send()
.await
.unwrap()
.status(),
404
);
let health = client.get(format!("{url}/health")).send().await.unwrap();
assert_eq!(health.status(), 200);
assert_eq!(
health.json::<serde_json::Value>().await.unwrap()["pid"],
state["pid"]
);
assert_eq!(
client
.post(format!("{url}/_lgtm/shutdown"))
.json(&serde_json::json!({}))
.send()
.await
.unwrap()
.status(),
404
);
let again = command(dir, &["up", "--port", "0", "--no-open"]);
assert!(again.status.success());
let reused = discovery(dir);
assert_eq!(state["pid"], reused["pid"]);
let foreground = command(dir, &["up", "--foreground", "--port", "0"]);
assert!(!foreground.status.success());
assert!(String::from_utf8_lossy(&foreground.stderr).contains("Another LGTM instance"));
let status = command(dir, &["status"]);
assert!(status.status.success());
let text = String::from_utf8_lossy(&status.stdout);
assert!(text.contains(&format!("UI: {url}")));
assert!(text.contains(&format!("Server: {url}")));
assert!(text.contains(&format!("Browser RPC: {url}/rpc")));
let status = command(dir, &["status", "--json"]);
let discovery: serde_json::Value = serde_json::from_slice(&status.stdout).unwrap();
assert_eq!(discovery["running"], true);
assert_eq!(discovery["pid"], state["pid"]);
assert_eq!(discovery["ui_url"], url);
assert_eq!(discovery["server_url"], url);
assert_eq!(discovery["rpc_url"], format!("{url}/rpc"));
assert!(discovery.get("token").is_none());
let cwd = dir
.canonicalize()
.unwrap()
.join("review space & plus+ café #file");
std::fs::create_dir(&cwd).unwrap();
let review = Command::new(env!("CARGO_BIN_EXE_lgtm"))
.env("LGTM_HOME", dir)
.args(["review", "--no-open"])
.current_dir(&cwd)
.output()
.unwrap();
assert!(review.status.success());
let review_url = reqwest::Url::parse(String::from_utf8_lossy(&review.stdout).trim()).unwrap();
assert_eq!(review_url.origin().ascii_serialization(), url);
let query: Vec<_> = review_url.query_pairs().collect();
assert_eq!(query, [("path".into(), cwd.to_str().unwrap().into())]);
assert_eq!(client.get(review_url).send().await.unwrap().status(), 200);
assert!(command(dir, &["down"]).status.success());
assert!(!dir.join("server.port").exists());
let status = command(dir, &["status"]);
assert!(String::from_utf8_lossy(&status.stdout).contains("not running"));
let status = command(dir, &["status", "--json"]);
assert_eq!(
serde_json::from_slice::<serde_json::Value>(&status.stdout).unwrap(),
serde_json::json!({"running": false})
);
let log = std::fs::read_to_string(dir.join("server.log")).unwrap();
assert!(log.contains("INFO") && log.contains("Server started"));
assert!(log.contains("Server stopped"));
assert!(!log.contains('\u{1b}'));
}
#[test]
fn occupied_port_fails_without_leaving_an_instance() {
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let port = listener.local_addr().unwrap().port().to_string();
let dir = tempfile::tempdir().unwrap();
let result = command(dir.path(), &["up", "--port", &port, "--no-open"]);
assert!(!result.status.success());
assert!(String::from_utf8_lossy(&result.stderr).contains("did not become ready"));
assert!(!dir.path().join("server.port").exists());
assert!(
std::fs::read_to_string(dir.path().join("server.log"))
.unwrap()
.contains("Address already in use")
);
}
#[test]
fn review_starts_a_ready_server_for_the_current_directory() {
let instance = Instance(tempfile::tempdir().unwrap());
let dir = instance.0.path();
let review = command(dir, &["review", "--port", "0", "--no-open"]);
assert!(
review.status.success(),
"{}",
String::from_utf8_lossy(&review.stderr)
);
let url = reqwest::Url::parse(String::from_utf8_lossy(&review.stdout).trim()).unwrap();
assert_eq!(
url.query_pairs().collect::<Vec<_>>(),
[(
"path".into(),
dir.canonicalize().unwrap().to_str().unwrap().into()
)]
);
let status = command(dir, &["status", "--json"]);
let discovery: serde_json::Value = serde_json::from_slice(&status.stdout).unwrap();
assert_eq!(discovery["running"], true);
assert_eq!(discovery["ui_url"], url.origin().ascii_serialization());
}
#[test]
fn simultaneous_up_calls_share_one_daemon() {
let instance = Instance(tempfile::tempdir().unwrap());
let dir = instance.0.path();
let spawn = || {
Command::new(env!("CARGO_BIN_EXE_lgtm"))
.env("LGTM_HOME", dir)
.args(["up", "--port", "0", "--no-open"])
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.unwrap()
};
let first = spawn();
let second = spawn();
let first = first.wait_with_output().unwrap();
let second = second.wait_with_output().unwrap();
for result in [&first, &second] {
assert!(
result.status.success(),
"{}",
String::from_utf8_lossy(&result.stderr)
);
assert!(
result.stderr.is_empty(),
"{}",
String::from_utf8_lossy(&result.stderr)
);
}
assert_eq!(first.stdout, second.stdout);
let state = discovery(dir);
let pid = std::fs::read_to_string(dir.join("server.pid")).unwrap();
assert_eq!(
pid.trim().parse::<u64>().unwrap(),
state["pid"].as_u64().unwrap()
);
assert!(!dir.join("start.lock").exists());
}
#[test]
fn stale_pid_does_not_stop_another_server() {
let first = Instance(tempfile::tempdir().unwrap());
let second = Instance(tempfile::tempdir().unwrap());
for instance in [&first, &second] {
assert!(
command(instance.0.path(), &["up", "--port", "0", "--no-open"])
.status
.success()
);
}
let first_info = discovery(first.0.path());
let second_info = discovery(second.0.path());
let stale = tempfile::tempdir().unwrap();
let url = reqwest::Url::parse(first_info["server_url"].as_str().unwrap()).unwrap();
std::fs::write(
stale.path().join("server.pid"),
second_info["pid"].to_string(),
)
.unwrap();
std::fs::write(
stale.path().join("server.port"),
url.port().unwrap().to_string(),
)
.unwrap();
let down = command(stale.path(), &["down"]);
assert!(down.status.success());
assert!(String::from_utf8_lossy(&down.stdout).contains("not running"));
for instance in [&first, &second] {
assert_eq!(discovery(instance.0.path())["running"], true);
}
}
#[test]
fn status_on_unused_home_does_not_create_files() {
let root = tempfile::tempdir().unwrap();
let home = root.path().join("unused home");
let status = Command::new(env!("CARGO_BIN_EXE_lgtm"))
.env("LGTM_HOME", &home)
.args(["status", "--json"])
.output()
.unwrap();
assert!(status.status.success());
assert_eq!(
serde_json::from_slice::<serde_json::Value>(&status.stdout).unwrap(),
serde_json::json!({"running": false})
);
assert!(!home.exists());
}
fn command(dir: &Path, args: &[&str]) -> Output {
Command::new(env!("CARGO_BIN_EXE_lgtm"))
.env("LGTM_HOME", dir)
.env("RUST_LOG", "info")
.args(args)
.current_dir(dir)
.output()
.unwrap()
}
fn discovery(dir: &Path) -> serde_json::Value {
let status = command(dir, &["status", "--json"]);
assert!(
status.status.success(),
"{}",
String::from_utf8_lossy(&status.stderr)
);
serde_json::from_slice(&status.stdout).unwrap()
}