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]
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
#[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);
|
||||
}
|
||||
}
|
||||
|
||||
#[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