use std::collections::HashMap;
use proptest::prelude::*;
use proptest::test_runner::TestCaseError;
use todoapp_app::{Anchor, Services};
use todoapp_core::testing::{FixedClock, SeqIds};
use todoapp_core::{Id, LinkKind, Status};
use todoapp_store_mem::MemStore;
fn rt() -> tokio::runtime::Runtime {
tokio::runtime::Builder::new_current_thread()
.build()
.unwrap()
}
fn fresh() -> (MemStore, FixedClock, SeqIds) {
(MemStore::new(), FixedClock::default(), SeqIds::default())
}
proptest! {
#[test]
fn blocks_graph_stays_acyclic(pairs in proptest::collection::vec((0usize..6, 0usize..6), 0..40)) {
rt().block_on(async {
let (store, clock, ids) = fresh();
let s = Services { store: &store, links: &store, collections: &store, query: &store, clock: &clock, ids: &ids, blobs: &store };
let mut nodes: Vec<Id> = Vec::new();
for _ in 0..6 {
nodes.push(s.create("n", None, Status::Todo, []).await.unwrap().id);
}
for (a, b) in pairs {
let _ = s.block(&nodes[a], &nodes[b]).await;
prop_assert!(acyclic(&store, &nodes).await);
}
Ok::<(), TestCaseError>(())
})?;
}
#[test]
fn child_order_is_consistent(n in 1usize..8, front in 0usize..8) {
rt().block_on(async {
let (store, clock, ids) = fresh();
let s = Services { store: &store, links: &store, collections: &store, query: &store, clock: &clock, ids: &ids, blobs: &store };
let p = s.create("p", None, Status::Todo, []).await.unwrap();
let mut kids: Vec<Id> = Vec::new();
for _ in 0..n {
kids.push(s.create("k", Some(&p.id), Status::Todo, []).await.unwrap().id);
}
let pos: Vec<f64> = s.children_of(&p.id).await.iter().map(|l| l.position.0).collect();
prop_assert!(pos.windows(2).all(|w| w[0] < w[1]));
let idx = front % n;
let first_now = s.children_of(&p.id).await[0].to.clone();
if s.children_of(&p.id).await[0].to != kids[idx] {
s.reorder(&kids[idx], Anchor::Before(first_now)).await.unwrap();
}
prop_assert_eq!(s.children_of(&p.id).await[0].to.clone(), kids[idx].clone());
let pos: Vec<f64> = s.children_of(&p.id).await.iter().map(|l| l.position.0).collect();
prop_assert_eq!(pos.len(), n);
prop_assert!(pos.windows(2).all(|w| w[0] < w[1]));
Ok::<(), TestCaseError>(())
})?;
}
}
async fn acyclic(store: &MemStore, nodes: &[Id]) -> bool {
use todoapp_core::LinkRepository;
let mut adj: HashMap<Id, Vec<Id>> = HashMap::new();
for n in nodes {
let outs = store.outgoing(n, LinkKind::Blocks).await;
adj.insert(n.clone(), outs.into_iter().map(|l| l.to).collect());
}
fn reaches(adj: &HashMap<Id, Vec<Id>>, from: &Id, target: &Id, seen: &mut Vec<Id>) -> bool {
for to in adj.get(from).into_iter().flatten() {
if to == target {
return true;
}
if !seen.contains(to) {
seen.push(to.clone());
if reaches(adj, to, target, seen) {
return true;
}
}
}
false
}
!nodes.iter().any(|n| reaches(&adj, n, n, &mut Vec::new()))
}