#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use std::collections::BTreeMap;
use std::time::Duration;
use stackless_core::fault::{Fault, codes};
use stackless_core::process::ProcessStamp;
use stackless_core::state::{InstanceStatus, Store};
fn store() -> Store {
Store::open_libsql_local(":memory:").expect("open libsql :memory:")
}
fn this_host() -> String {
sysinfo::System::host_name().unwrap_or_default()
}
fn inject_holder(store: &Store, host: &str, pid: u32, start: i64, acquired_at: i64) {
store
.execute_for_tests(
"INSERT INTO op_locks
(instance, operation, holder_pid, holder_start_time, holder_host, acquired_at)
VALUES ('demo', 'up', ?1, ?2, ?3, ?4)
ON CONFLICT(instance) DO UPDATE SET
holder_pid = excluded.holder_pid,
holder_start_time = excluded.holder_start_time,
holder_host = excluded.holder_host,
acquired_at = excluded.acquired_at",
&[
&pid.to_string(),
&start.to_string(),
host,
&acquired_at.to_string(),
],
)
.unwrap();
}
#[test]
fn migrations_run_and_are_idempotent_on_libsql() {
let _ = store();
let _ = store();
}
#[test]
fn instance_records_round_trip() {
let store = store();
let rec = store
.create_instance("demo", "local", "deftext", &BTreeMap::new(), "/defs", false)
.unwrap();
assert_eq!(rec.name.as_str(), "demo");
assert_eq!(rec.substrate.as_str(), "local");
assert_eq!(rec.status, InstanceStatus::Active);
assert_eq!(rec.definition, "deftext");
assert_eq!(rec.definition_dir, "/defs");
assert!(rec.tombstoned_at.is_none());
let fetched = store.instance("demo").unwrap().unwrap();
assert_eq!(fetched.name.as_str(), "demo");
assert_eq!(store.instances().unwrap().len(), 1);
}
#[test]
fn names_are_unique_across_substrates() {
let store = store();
store
.create_instance("demo", "local", "d", &BTreeMap::new(), "", false)
.unwrap();
let err = store
.create_instance("demo", "render", "d", &BTreeMap::new(), "", false)
.unwrap_err();
assert_eq!(err.code(), codes::STATE_INSTANCE_EXISTS);
assert!(err.to_string().contains("local"));
}
#[test]
fn tombstone_and_revive() {
let store = store();
store
.create_instance("demo", "local", "v1", &BTreeMap::new(), "", false)
.unwrap();
store
.record_checkpoint("demo", "s1", "process", "111", "{}")
.unwrap();
store.tombstone_instance("demo").unwrap();
let rec = store.instance("demo").unwrap().unwrap();
assert_eq!(rec.status, InstanceStatus::Tombstoned);
assert!(rec.tombstoned_at.is_some());
store
.revive_instance("demo", "v2", &BTreeMap::new(), false)
.unwrap();
let rec = store.instance("demo").unwrap().unwrap();
assert_eq!(rec.status, InstanceStatus::Active);
assert_eq!(rec.definition, "v2");
assert!(store.checkpoint("demo", "s1").unwrap().is_none());
}
#[test]
fn leases_set_renew_and_expire() {
let store = store();
store
.create_instance("fresh", "local", "d", &BTreeMap::new(), "", false)
.unwrap();
store
.create_instance("stale", "local", "d", &BTreeMap::new(), "", false)
.unwrap();
let lease = store
.renew_lease("fresh", Duration::from_secs(3600))
.unwrap();
assert_eq!(lease.duration, Duration::from_secs(3600));
assert!(lease.remaining(Store::now_secs()) > Duration::from_secs(3000));
store.renew_lease("stale", Duration::from_secs(0)).unwrap();
assert_eq!(store.expired_instances().unwrap(), vec!["stale"]);
store.renew_lease_at_recorded_duration("fresh").unwrap();
assert!(store.lease("fresh").unwrap().is_some());
store.delete_lease("fresh").unwrap();
assert!(store.lease("fresh").unwrap().is_none());
}
#[test]
fn journal_round_trips_payloads() {
let store = store();
store
.create_instance("demo", "local", "d", &BTreeMap::new(), "", false)
.unwrap();
store
.record_checkpoint("demo", "start:api", "process", "12345", r#"{"port":8080}"#)
.unwrap();
let cp = store.checkpoint("demo", "start:api").unwrap().unwrap();
assert_eq!(cp.resource_kind, "process");
assert_eq!(cp.resource_id, "12345");
assert_eq!(cp.payload, r#"{"port":8080}"#);
assert_eq!(store.checkpoints("demo").unwrap().len(), 1);
store.remove_checkpoint("demo", "start:api").unwrap();
assert!(store.checkpoint("demo", "start:api").unwrap().is_none());
}
#[test]
fn lock_claim_release_and_liveness() {
let store = store();
store
.create_instance("demo", "local", "d", &BTreeMap::new(), "", false)
.unwrap();
let claim = store.claim_lock("demo", "up").unwrap();
assert!(store.lock_holder_alive("demo").unwrap());
store.release_lock(&claim).unwrap();
assert!(!store.lock_holder_alive("demo").unwrap());
}
#[test]
fn libsql_self_reclaim_succeeds() {
let store = store();
store
.create_instance("demo", "local", "d", &BTreeMap::new(), "", false)
.unwrap();
let claim = store.claim_lock("demo", "up").unwrap();
store.claim_lock("demo", "verify").unwrap();
store.release_lock(&claim).unwrap();
}
#[test]
fn libsql_dead_same_host_holder_is_taken_over() {
let store = store();
store
.create_instance("demo", "local", "d", &BTreeMap::new(), "", false)
.unwrap();
inject_holder(
&store,
&this_host(),
std::process::id(),
1,
Store::now_secs(),
);
let dead = ProcessStamp {
pid: stackless_core::types::Pid::from_os(std::process::id()),
start_time: stackless_core::types::ProcessStartTime::from_os(1),
};
assert!(!dead.is_alive());
store.claim_lock("demo", "down").unwrap();
}
#[test]
fn libsql_fresh_foreign_holder_is_respected() {
let store = store();
store
.create_instance("demo", "local", "d", &BTreeMap::new(), "", false)
.unwrap();
inject_holder(&store, "other-machine", 4242, 7, Store::now_secs());
let err = store.claim_lock("demo", "up").unwrap_err();
assert_eq!(err.code(), codes::STATE_LOCK_HELD);
}
#[test]
fn libsql_stale_foreign_holder_is_taken_over() {
let store = store();
store
.create_instance("demo", "local", "d", &BTreeMap::new(), "", false)
.unwrap();
inject_holder(
&store,
"other-machine",
4242,
7,
Store::now_secs() - 31 * 60,
);
store.claim_lock("demo", "down").unwrap();
}
#[test]
fn reaper_failure_bookkeeping_and_gc() {
let store = store();
store
.create_instance("demo", "local", "d", &BTreeMap::new(), "", false)
.unwrap();
store.record_reap_failure("demo", "boom").unwrap();
let attempt = store.reap_attempt("demo").unwrap().unwrap();
assert_eq!(attempt.attempts, 1);
assert_eq!(attempt.last_error, "boom");
store.record_reap_failure("demo", "boom again").unwrap();
assert_eq!(store.reap_attempt("demo").unwrap().unwrap().attempts, 2);
store.clear_reap_failure("demo").unwrap();
assert!(store.reap_attempt("demo").unwrap().is_none());
store.tombstone_instance("demo").unwrap();
assert!(store.gc_due_tombstones().unwrap().is_empty());
store.delete_instance("demo").unwrap();
assert!(store.instance("demo").unwrap().is_none());
}