Merge pull request #101 from Miyabi-G-K/feature/issue-100-proptest
test(core): proptest property-based tests for lock/store
This commit is contained in:
commit
92c551f1b7
3 changed files with 154 additions and 0 deletions
|
|
@ -32,3 +32,4 @@ toml = "0.8"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
|
proptest = "1"
|
||||||
|
|
|
||||||
88
crates/miyabi-core/src/lock.rs
generated
88
crates/miyabi-core/src/lock.rs
generated
|
|
@ -495,3 +495,91 @@ mod tests {
|
||||||
assert!(sweep.active.is_empty());
|
assert!(sweep.active.is_empty());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod proptest_tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::store::{CompletionMode, EventStore, ExecutionTask, SnapshotStore};
|
||||||
|
use proptest::prelude::*;
|
||||||
|
use tempfile::TempDir;
|
||||||
|
|
||||||
|
fn arb_file_name() -> impl Strategy<Value = String> {
|
||||||
|
prop::string::string_regex("[a-z]{1,8}/[a-z]{1,8}\\.rs")
|
||||||
|
.unwrap()
|
||||||
|
.prop_filter("non-empty", |s| !s.is_empty())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn arb_task_id() -> impl Strategy<Value = String> {
|
||||||
|
prop::string::string_regex("task-[a-z]{1,4}")
|
||||||
|
.unwrap()
|
||||||
|
.prop_filter("non-empty", |s| !s.is_empty())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup() -> (TempDir, SnapshotStore, FileLockManager) {
|
||||||
|
let tmp = TempDir::new().unwrap();
|
||||||
|
let event_store = EventStore::new(tmp.path().join("events.jsonl"));
|
||||||
|
let snapshot_store = SnapshotStore::new(
|
||||||
|
tmp.path().join("tasks.snapshot.json"),
|
||||||
|
tmp.path().join(".tasks.lock"),
|
||||||
|
);
|
||||||
|
let manager = FileLockManager::new(
|
||||||
|
event_store,
|
||||||
|
snapshot_store.clone(),
|
||||||
|
LeaseConfig {
|
||||||
|
lease_duration_sec: 300,
|
||||||
|
heartbeat_interval_sec: 60,
|
||||||
|
stale_after_missed_heartbeats: 2,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
(tmp, snapshot_store, manager)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn seed(snapshot_store: &SnapshotStore, id: &str) {
|
||||||
|
let mut snapshot = snapshot_store.load().unwrap();
|
||||||
|
let mut task = ExecutionTask::new(id, format!("Task {id}"));
|
||||||
|
task.completion_mode = CompletionMode::GithubPr;
|
||||||
|
snapshot.upsert_task(task);
|
||||||
|
let version = snapshot.version;
|
||||||
|
snapshot_store.save(&snapshot, version).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
proptest! {
|
||||||
|
#[test]
|
||||||
|
fn acquire_then_release_always_clears(
|
||||||
|
file in arb_file_name()
|
||||||
|
) {
|
||||||
|
let (_tmp, ss, mgr) = setup();
|
||||||
|
seed(&ss, "task-a");
|
||||||
|
mgr.acquire_lock("task-a", "agent", "node", &[file.clone()]).unwrap();
|
||||||
|
let c = mgr.has_conflict(&[file.clone()]).unwrap();
|
||||||
|
prop_assert!(c.conflicting);
|
||||||
|
mgr.release_lock("task-a").unwrap();
|
||||||
|
let c = mgr.has_conflict(&[file]).unwrap();
|
||||||
|
prop_assert!(!c.conflicting);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn two_tasks_cannot_lock_same_file(
|
||||||
|
file in arb_file_name()
|
||||||
|
) {
|
||||||
|
let (_tmp, ss, mgr) = setup();
|
||||||
|
seed(&ss, "task-a");
|
||||||
|
seed(&ss, "task-b");
|
||||||
|
mgr.acquire_lock("task-a", "agent", "node", &[file.clone()]).unwrap();
|
||||||
|
let result = mgr.acquire_lock("task-b", "agent", "node", &[file]);
|
||||||
|
prop_assert!(result.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn renew_by_wrong_owner_always_fails(
|
||||||
|
task_id in arb_task_id(),
|
||||||
|
file in arb_file_name()
|
||||||
|
) {
|
||||||
|
let (_tmp, ss, mgr) = setup();
|
||||||
|
seed(&ss, &task_id);
|
||||||
|
mgr.acquire_lock(&task_id, "owner", "node", &[file]).unwrap();
|
||||||
|
let result = mgr.renew_lease(&task_id, "intruder", "other");
|
||||||
|
prop_assert!(result.is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
65
crates/miyabi-core/src/store.rs
generated
65
crates/miyabi-core/src/store.rs
generated
|
|
@ -1070,3 +1070,68 @@ mod tests {
|
||||||
assert_eq!(result_zero, base);
|
assert_eq!(result_zero, base);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod proptest_tests {
|
||||||
|
use super::*;
|
||||||
|
use proptest::prelude::*;
|
||||||
|
use tempfile::TempDir;
|
||||||
|
|
||||||
|
fn sample_task(id: &str) -> ExecutionTask {
|
||||||
|
ExecutionTask::new(id, format!("Task {id}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
proptest! {
|
||||||
|
#[test]
|
||||||
|
fn cas_rejects_stale_version(version in 1u64..100) {
|
||||||
|
let tmp = TempDir::new().unwrap();
|
||||||
|
let store = SnapshotStore::new(
|
||||||
|
tmp.path().join("snap.json"),
|
||||||
|
tmp.path().join(".lock"),
|
||||||
|
);
|
||||||
|
let mut snap = TasksSnapshot::default();
|
||||||
|
snap.upsert_task(sample_task("t"));
|
||||||
|
store.save(&snap, 0).unwrap();
|
||||||
|
|
||||||
|
// Try saving with wrong version (0, already bumped to 1)
|
||||||
|
let result = store.save(&snap, 0);
|
||||||
|
prop_assert!(result.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn upsert_is_idempotent(n in 1usize..10) {
|
||||||
|
let tmp = TempDir::new().unwrap();
|
||||||
|
let store = SnapshotStore::new(
|
||||||
|
tmp.path().join("snap.json"),
|
||||||
|
tmp.path().join(".lock"),
|
||||||
|
);
|
||||||
|
let mut snap = TasksSnapshot::default();
|
||||||
|
for _ in 0..n {
|
||||||
|
snap.upsert_task(sample_task("same-id"));
|
||||||
|
}
|
||||||
|
prop_assert_eq!(snap.tasks.len(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn event_replay_is_deterministic(count in 1usize..20) {
|
||||||
|
let tmp = TempDir::new().unwrap();
|
||||||
|
let es = EventStore::new(tmp.path().join("events.jsonl"));
|
||||||
|
for i in 0..count {
|
||||||
|
es.append(&TaskEvent {
|
||||||
|
id: format!("e-{i}"),
|
||||||
|
ts: Utc::now(),
|
||||||
|
event_type: TaskEventType::GatePassed,
|
||||||
|
task_id: "task-a".into(),
|
||||||
|
agent: "t".into(),
|
||||||
|
node: "t".into(),
|
||||||
|
payload: serde_json::json!({}),
|
||||||
|
version: i as u64,
|
||||||
|
}).unwrap();
|
||||||
|
}
|
||||||
|
let r1 = es.replay(None).unwrap();
|
||||||
|
let r2 = es.replay(None).unwrap();
|
||||||
|
prop_assert_eq!(r1.len(), r2.len());
|
||||||
|
prop_assert_eq!(r1.len(), count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue