use std::io;
use std::num::NonZeroU32;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use haematite::db::DatabaseConfig;
use haematite::store::NodeCacheBudget;
use serde_json::{Value, json};
use crate::serve::protocol::{BranchPage, Operation, Version};
use crate::serve::service::Service;
use crate::serve::storage::{Failure, Storage};
type TestResult = Result<(), Box<dyn std::error::Error>>;
const LIMIT: NonZeroU32 = NonZeroU32::MIN.saturating_add(9);
const EXPIRES: u64 = 2_000_000_000;
const WAIT: Duration = Duration::from_secs(30);
fn with_service(test: impl FnOnce(&Service) -> TestResult) -> TestResult {
let directory = tempfile::tempdir()?;
let storage = Storage::create(
DatabaseConfig {
data_dir: directory.path().join("database"),
shard_count: 1,
distributed: None,
executor_threads: Some(1),
node_cache_budget: Some(NodeCacheBudget::Unlimited),
retention: haematite::Retention::KeepAll,
},
NodeCacheBudget::Unlimited,
fixture_long_wait_interval(),
)?;
let service = Service::new(storage, LIMIT)?;
test(&service)
}
fn head(service: &Service, branch: &str) -> Result<Version, Box<dyn std::error::Error>> {
let answer = service.dispatch(Operation::Branches(BranchPage {
after: None,
limit: LIMIT,
}))?;
let row = answer["entries"]
.as_array()
.ok_or("branches has no entries")?
.iter()
.find(|row| row["branch"] == branch)
.ok_or_else(|| format!("branch {branch} missing"))?;
Ok(serde_json::from_value(row["version"].clone())?)
}
fn mutate(service: &Service, branch: &str, request_id: &str, change: &Value) -> TestResult {
let expected = head(service, branch)?;
let operation: Operation = serde_json::from_value(json!({
"method": "mutation.apply",
"params": {"request_id": request_id, "branch": branch, "expected": expected, "change": change},
}))?;
service.dispatch(operation)?;
Ok(())
}
fn collection(name: &str) -> Value {
json!({"kind": "collection.put", "value": {"collection": name, "schema": null}})
}
fn fork_trial(service: &Service) -> TestResult {
let from = head(service, "main")?;
mutate(
service,
"main",
"fork-trial",
&json!({"kind": "branch.fork", "value": {"name": "trial", "from": from}}),
)
}
fn at(seconds: u64) -> SystemTime {
UNIX_EPOCH + Duration::from_secs(seconds)
}
fn frame(branch: &str, version: Version) -> Value {
json!({"branch": branch, "version": version})
}
fn with_follower(
service: &Service,
branch: &str,
steps: impl FnOnce(&mpsc::Receiver<Value>) -> TestResult,
) -> Result<(TestResult, Failure, Vec<Value>), Box<dyn std::error::Error>> {
let late = AtomicBool::new(false);
let (sent, frames) = mpsc::channel();
std::thread::scope(|scope| {
let follower = scope.spawn(|| {
service.follow(
branch,
Some(EXPIRES),
|| {
if late.load(Ordering::SeqCst) {
at(EXPIRES)
} else {
at(EXPIRES - 600)
}
},
move |head: &Value| {
sent.send(head.clone())
.map_err(|error| io::Error::new(io::ErrorKind::BrokenPipe, error))
},
)
});
let outcome = steps(&frames);
late.store(true, Ordering::SeqCst);
mutate(service, "main", "wake", &collection("woken"))?;
let ended = follower.join().map_err(|_| "the follower panicked")??;
let after: Vec<Value> = frames.try_iter().collect();
Ok((outcome, ended, after))
})
}
#[test]
fn a_follower_gets_its_head_then_one_frame_per_publish_of_its_branch() -> TestResult {
with_service(|service| {
fork_trial(service)?;
let (outcome, ended, after) = with_follower(service, "main", |frames| {
assert_eq!(
frames.recv_timeout(WAIT)?,
frame("main", head(service, "main")?)
);
mutate(service, "main", "one", &collection("one"))?;
assert_eq!(
frames.recv_timeout(WAIT)?,
frame("main", head(service, "main")?)
);
mutate(service, "trial", "other", &collection("other"))?;
mutate(service, "main", "two", &collection("two"))?;
assert_eq!(
frames.recv_timeout(WAIT)?,
frame("main", head(service, "main")?)
);
Ok(())
})?;
outcome?;
assert_eq!(ended.code, "pass_expired");
assert!(after.is_empty(), "frames after the publishes: {after:?}");
Ok(())
})
}
#[test]
fn a_follow_ends_pass_expired_at_the_passs_expiry() -> TestResult {
with_service(|service| {
let (outcome, ended, after) = with_follower(service, "main", |frames| {
frames.recv_timeout(WAIT)?;
Ok(())
})?;
outcome?;
assert_eq!(ended.code, "pass_expired");
assert!(after.is_empty(), "a head past the expiry: {after:?}");
Ok(())
})
}
#[test]
fn an_expired_pass_is_sent_no_head() -> TestResult {
with_service(|service| {
let mut sent = Vec::new();
let ended = service.follow(
"main",
Some(EXPIRES),
|| at(EXPIRES + 1),
|head: &Value| {
sent.push(head.clone());
Ok(())
},
)?;
assert_eq!(ended.code, "pass_expired");
assert!(sent.is_empty(), "an expired pass was sent {sent:?}");
Ok(())
})
}
#[test]
fn a_follow_ends_branch_unknown_when_its_branch_is_removed() -> TestResult {
with_service(|service| {
fork_trial(service)?;
let (outcome, ended, after) = with_follower(service, "trial", |frames| {
assert_eq!(
frames.recv_timeout(WAIT)?,
frame("trial", head(service, "trial")?)
);
mutate(
service,
"trial",
"discard",
&json!({"kind": "branch.discard", "value": {"name": "trial"}}),
)?;
Ok(())
})?;
outcome?;
assert_eq!(ended.code, "branch_unknown");
assert!(after.is_empty(), "frames after the discard: {after:?}");
let mut sent = Vec::new();
let never = service.follow("absent", None, SystemTime::now, |head: &Value| {
sent.push(head.clone());
Ok(())
})?;
assert_eq!(never.code, "branch_unknown");
assert!(sent.is_empty());
Ok(())
})
}
const fn fixture_long_wait_interval() -> std::time::Duration {
std::time::Duration::from_secs(1)
}