use repolith_cache::{Cache, SqliteCache};
use repolith_core::types::{ActionId, BuildError, BuildEvent, Sha256};
fn success(id: &str, ms: u64) -> BuildEvent {
BuildEvent::Success {
id: ActionId(id.to_string()),
input: Sha256([3; 32]),
output: Sha256([4; 32]),
ms,
}
}
fn failed(id: &str) -> BuildEvent {
BuildEvent::Failed {
id: ActionId(id.to_string()),
input: Sha256([5; 32]),
error: BuildError::Io("disk full".to_string()),
ms: 7,
}
}
async fn run_contract_suite(cache: &mut dyn Cache, ns: &str) {
let id = |s: &str| ActionId(format!("{ns}::{s}"));
assert!(
cache.last_build(&id("absent")).await.is_none(),
"unknown id must return None"
);
cache
.record(success(&id("ok").0, 42))
.await
.expect("record success");
match cache.last_build(&id("ok")).await.expect("event stored") {
BuildEvent::Success {
input, output, ms, ..
} => {
assert_eq!(input, Sha256([3; 32]));
assert_eq!(output, Sha256([4; 32]));
assert_eq!(ms, 42);
}
BuildEvent::Failed { .. } => panic!("expected Success"),
}
cache
.record(failed(&id("ko").0))
.await
.expect("record failed");
match cache.last_build(&id("ko")).await.expect("event stored") {
BuildEvent::Failed { error, .. } => {
assert!(
matches!(&error, BuildError::Io(m) if m == "disk full"),
"typed error must roundtrip, got: {error:?}"
);
}
BuildEvent::Success { .. } => panic!("expected Failed"),
}
cache
.record(success(&id("ok").0, 99))
.await
.expect("overwrite");
match cache.last_build(&id("ok")).await.expect("event stored") {
BuildEvent::Success { ms, .. } => assert_eq!(ms, 99, "latest event wins"),
BuildEvent::Failed { .. } => panic!("expected Success"),
}
cache
.record(failed(&id("ok").0))
.await
.expect("flip to failed");
assert!(
matches!(
cache.last_build(&id("ok")).await,
Some(BuildEvent::Failed { .. })
),
"status flip must be visible"
);
let batch: Vec<BuildEvent> = (0..5)
.map(|i| success(&id(&format!("b{i}")).0, i))
.collect();
cache.record_batch(batch).await.expect("record_batch");
for i in 0..5u64 {
match cache.last_build(&id(&format!("b{i}"))).await {
Some(BuildEvent::Success { ms, .. }) => assert_eq!(ms, i),
other => panic!("batch event b{i} missing or wrong: {other:?}"),
}
}
cache.record_batch(vec![]).await.expect("empty batch ok");
}
#[tokio::test]
async fn sqlite_satisfies_the_contract() {
let mut cache = SqliteCache::in_memory().expect("in-memory sqlite");
run_contract_suite(&mut cache, "run").await;
}
#[tokio::test]
async fn sqlite_file_backed_satisfies_the_contract() {
let dir = tempfile::tempdir().expect("tempdir");
let mut cache = SqliteCache::open(dir.path().join("contract.db")).expect("open");
run_contract_suite(&mut cache, "run").await;
}
#[cfg(feature = "neo4j")]
mod neo4j_contract {
use super::run_contract_suite;
use repolith_cache::{Neo4jCache, Neo4jConfig};
#[tokio::test]
#[ignore = "requires a live Neo4j server + REPOLITH_NEO4J_* env vars"]
async fn neo4j_satisfies_the_contract() {
let cfg = Neo4jConfig::from_env().expect("REPOLITH_NEO4J_* env vars");
let mut cache = Neo4jCache::connect(&cfg).await.expect("connect");
let ns = format!(
"contract-{}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("clock after epoch")
.as_nanos()
);
run_contract_suite(&mut cache, &ns).await;
}
}