mod common;
use std::io::{BufRead, BufReader};
use std::process::Stdio;
use std::sync::mpsc;
use std::time::Duration;
use common::*;
fn spawn_watch(cli: &Cli, args: &[&str]) -> (std::process::Child, mpsc::Receiver<String>) {
let mut cmd = cli.command(args);
cmd.stdout(Stdio::piped()).stderr(Stdio::inherit());
let mut child = cmd.spawn().unwrap();
let stdout = child.stdout.take().unwrap();
let (tx, rx) = mpsc::channel();
std::thread::spawn(move || {
for line in BufReader::new(stdout).lines().map_while(Result::ok) {
if tx.send(line).is_err() {
break;
}
}
});
(child, rx)
}
struct Running(std::process::Child);
impl Drop for Running {
fn drop(&mut self) {
let _ = self.0.kill();
let _ = self.0.wait();
}
}
async fn next_push(rx: &mpsc::Receiver<String>, secs: u64) -> Option<serde_json::Value> {
let deadline = std::time::Instant::now() + Duration::from_secs(secs);
loop {
let left = deadline.saturating_duration_since(std::time::Instant::now());
if left.is_zero() {
return None;
}
match rx.try_recv() {
Ok(line) => {
if let Ok(v) = serde_json::from_str::<serde_json::Value>(&line)
&& v.get("rev").is_some()
{
return Some(v);
}
}
Err(mpsc::TryRecvError::Empty) => tokio::time::sleep(Duration::from_millis(50)).await,
Err(mpsc::TryRecvError::Disconnected) => return None,
}
}
}
#[tokio::test(flavor = "multi_thread")]
async fn watch_repushes_a_folder_once_per_burst() {
let server = Server::start().await;
let cli = Cli::new(&server);
cli.write("site/index.md", "# One");
cli.write("site/other.md", "# Other");
let (mut child, rx) = spawn_watch(&cli, &["watch", "site", "--json"]);
let first = next_push(&rx, 15).await.expect("initial push");
assert_eq!(first["rev"], 1);
assert!(
next_push(&rx, 2).await.is_none(),
"no push without a change"
);
for n in 0..4 {
cli.write("site/index.md", format!("# Edit {n}"));
tokio::time::sleep(Duration::from_millis(80)).await;
}
cli.write("site/other.md", "# Other changed");
let second = next_push(&rx, 15).await.expect("push after edits");
assert_eq!(second["rev"], 2);
assert_eq!(second["id"], first["id"]);
assert!(
next_push(&rx, 2).await.is_none(),
"the burst was coalesced into one push"
);
cli.write("site/.notes", "scratch");
assert!(next_push(&rx, 2).await.is_none());
child.kill().unwrap();
let _ = child.wait();
}
#[tokio::test(flavor = "multi_thread")]
async fn watch_a_single_file() {
let server = Server::start().await;
let cli = Cli::new(&server);
cli.write("doc.md", "# Start");
cli.write("unrelated.md", "# Unrelated");
let (mut child, rx) = spawn_watch(&cli, &["watch", "doc.md", "--json"]);
let first = next_push(&rx, 15).await.expect("initial push");
cli.write("unrelated.md", "# Changed but not watched");
assert!(
next_push(&rx, 2).await.is_none(),
"other files in the folder do not count"
);
cli.write("doc.md", "# Changed");
let second = next_push(&rx, 15).await.expect("push after edit");
assert_eq!(second["id"], first["id"]);
assert_eq!(second["rev"], 2);
child.kill().unwrap();
let _ = child.wait();
}
#[tokio::test(flavor = "multi_thread")]
async fn watch_in_a_project_pushes_by_key() {
let server = Server::start().await;
let cli = Cli::new(&server);
cli.write("docs/plan.md", "# One");
assert_eq!(
cli.run(&["project", "init", ".", "--name", "semora"])
.await
.code,
0
);
let (child, rx) = spawn_watch(&cli, &["watch", "docs/plan.md", "--json"]);
let _child = Running(child);
let first = next_push(&rx, 15).await.expect("initial push");
assert_eq!(first["project"], "semora");
assert_eq!(first["rev"], 1);
let gone = cli.run(&["rm", first["id"].as_str().unwrap()]).await;
assert_eq!(gone.code, 0, "{}", gone.stderr);
cli.write("docs/plan.md", "# Two");
let second = next_push(&rx, 15).await.expect("push after the edit");
assert_ne!(
second["id"], first["id"],
"pushed by key, so a deleted document is made again"
);
assert_eq!(second["rev"], 1);
assert_eq!(second["path"], "docs/plan.md");
cli.write("docs/plan.md", "# Three");
let third = next_push(&rx, 15).await.expect("another push");
assert_eq!(third["id"], second["id"]);
assert_eq!(third["rev"], 2);
}
#[tokio::test(flavor = "multi_thread")]
async fn watch_with_id_in_a_project_keeps_pushing_to_that_document() {
let server = Server::start().await;
let cli = Cli::new(&server);
cli.write("docs/plan.md", "# One");
assert_eq!(
cli.run(&["project", "init", ".", "--name", "semora"])
.await
.code,
0
);
let standalone = cli
.run(&["push", "docs/plan.md", "--new", "--json"])
.await
.json();
let id = standalone["id"].as_str().unwrap().to_string();
let (child, rx) = spawn_watch(&cli, &["watch", "docs/plan.md", "--id", &id, "--json"]);
let child = Running(child);
let first = next_push(&rx, 15).await.expect("initial push");
assert_eq!(first["id"], id.as_str());
assert_eq!(first["rev"], 2);
cli.write("docs/plan.md", "# Two");
let second = next_push(&rx, 15).await.expect("push after the edit");
assert_eq!(
second["id"],
id.as_str(),
"still X, not the document at the key"
);
assert_eq!(second["rev"], 3);
drop(child);
let projects = cli.run(&["project", "ls", "--json"]).await.json();
assert_eq!(
projects[0]["documents"], 0,
"no document was made at the key"
);
}