use std::collections::HashMap;
use proptest::prelude::*;
use super::test_common::{TestLong, TestShort, World, long_data, short_data};
use crate::notes::relations::{ChainForward, ParentChild, SameStartTime};
use crate::notes::{
GroupId, Interval, LongNoteTypeStorage, NotesStorage, NotesStorageEditHook,
ShortNoteTypeStorage,
};
use crate::rhythm::{ClockTime, Time};
crate::generate_notes_storage! {
enum TestNoteType,
#[derive(PartialEq)]
enum TestNote,
#[derive(Debug, PartialEq)]
enum TestNoteSnapshot,
enum TestNoteRef,
enum TestNoteMut,
#[derive(Debug, Clone)]
struct TestNotesStorage {
note_type Short(TestShort) => shorts: ShortNoteTypeStorage<TestShort>,
note_type Long(TestLong) => longs: LongNoteTypeStorage<TestLong>,
field hello: f64,
field hook_log: Vec<HookEvent>,
field hook_sizes: Vec<usize>,
field hook_batches: Vec<usize>,
edit_hook EditHook,
}
}
impl TestNotesStorage {
fn health_check(&self) {
self.shorts.health_check();
self.longs.health_check();
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum TestNoteId {
Short(u64),
Long(u64),
}
#[derive(Debug, Clone)]
enum TestNoteWithId {
Short(u64, TestShort),
Long(u64, TestLong),
}
impl From<TestNoteWithId> for TestNote {
fn from(val: TestNoteWithId) -> Self {
match val {
TestNoteWithId::Short(_, n) => TestNote::Short(n),
TestNoteWithId::Long(_, n) => TestNote::Long(n),
}
}
}
impl From<TestNoteWithId> for TestNoteId {
fn from(val: TestNoteWithId) -> Self {
match val {
TestNoteWithId::Short(id, _) => TestNoteId::Short(id),
TestNoteWithId::Long(id, _) => TestNoteId::Long(id),
}
}
}
#[derive(Debug, Clone, PartialEq)]
enum HookEvent {
PreAdd(TestNote),
PostAdd(TestNoteType, u64),
PreRemove(TestNoteType, u64),
PostRemove(TestNote, u64),
PreUpdate(u64, TestNote),
PostUpdate(u64, TestNote, TestNote),
}
struct EditHook;
impl crate::notes::generator::notes_storage::NotesStorageEditHook<TestNotesStorage> for EditHook {
fn pre_add(&mut self, storage: &mut TestNotesStorage, notes: &mut [TestNote]) {
storage.hook_batches.push(notes.len());
for note in notes {
storage.hook_log.push(HookEvent::PreAdd(note.clone()));
}
}
fn post_add(&mut self, storage: &mut TestNotesStorage, notes: &[(u64, TestNoteType)]) {
storage.hook_batches.push(notes.len());
for &(note_id, note_type) in notes {
storage.hook_sizes.push(storage.all_notes().count());
storage.hello += note_id as f64;
storage
.hook_log
.push(HookEvent::PostAdd(note_type, note_id));
}
}
fn pre_remove(&mut self, storage: &mut TestNotesStorage, notes: &[TestNoteSnapshot]) {
storage.hook_batches.push(notes.len());
for note in notes {
let note_type = note.note_type();
let note_id = note.id();
storage.hook_sizes.push(storage.all_notes().count());
storage
.hook_log
.push(HookEvent::PreRemove(note_type, note_id));
}
}
fn post_remove(&mut self, storage: &mut TestNotesStorage, notes: &[TestNoteSnapshot]) {
storage.hook_batches.push(notes.len());
for note in notes {
storage.hook_sizes.push(storage.all_notes().count());
assert_eq!(
note.clock_time_interval(),
Interval::new(
clock_of(¬e.start_time()),
clock_of(¬e.start_time()) + clock_of(¬e.duration())
)
);
storage
.hook_log
.push(HookEvent::PostRemove(note.to_owned_note(), note.id()));
}
}
fn pre_update(&mut self, storage: &mut TestNotesStorage, notes: &mut [(u64, TestNote)]) {
storage.hook_batches.push(notes.len());
for (note_id, note) in notes {
storage
.hook_log
.push(HookEvent::PreUpdate(*note_id, note.clone()));
}
}
fn post_update(
&mut self,
storage: &mut TestNotesStorage,
notes: &[(TestNoteSnapshot, TestNoteSnapshot)],
) {
storage.hook_batches.push(notes.len());
for (before, after) in notes {
storage.hook_log.push(HookEvent::PostUpdate(
before.id(),
before.to_owned_note(),
after.to_owned_note(),
));
assert_eq!(before.id(), after.id());
assert_eq!(
before.clock_time_interval(),
Interval::new(
clock_of(&before.start_time()),
clock_of(&before.start_time()) + clock_of(&before.duration())
)
);
assert_eq!(
after.clock_time_interval(),
Interval::new(
clock_of(&after.start_time()),
clock_of(&after.start_time()) + clock_of(&after.duration())
)
);
}
}
}
impl World {
fn build_notes_storage(&mut self, notes: Vec<TestNote>) -> TestNotesStorage {
TestNotesStorage::with_notes(
notes,
&self.note_groups,
&self.tempo_tracks,
&self.scroll_tracks,
)
}
fn add_note(&mut self, storage: &mut TestNotesStorage, note: TestNote) {
storage.add_note(
&self.note_groups,
&self.tempo_tracks,
&self.scroll_tracks,
note,
);
}
fn add_notes(&mut self, storage: &mut TestNotesStorage, notes: Vec<TestNote>) {
storage.add_notes(
&self.note_groups,
&self.tempo_tracks,
&self.scroll_tracks,
notes,
);
}
fn replace_note(&mut self, storage: &mut TestNotesStorage, note: TestNoteWithId) {
let (id, note) = with_id_parts(note);
storage.replace_note(
&self.note_groups,
&self.tempo_tracks,
&self.scroll_tracks,
id,
note,
);
}
fn replace_notes(&mut self, storage: &mut TestNotesStorage, notes: Vec<TestNoteWithId>) {
storage.replace_notes(
&self.note_groups,
&self.tempo_tracks,
&self.scroll_tracks,
notes.into_iter().map(with_id_parts).collect(),
);
}
}
fn group_id_from_index(i: usize) -> GroupId {
if i == 0 {
GroupId::Main
} else {
GroupId::Named(i as u64 - 1)
}
}
fn arb_short(group_count: usize) -> impl Strategy<Value = TestNote> {
(-600000..600000i32, -10.0..10.0f32, 0..=group_count).prop_map(move |(t, pos, gi)| {
TestNote::Short(TestShort {
time: Time::Clock(ClockTime::from_milis(t as f64).unwrap()),
gr_id: group_id_from_index(gi),
pos,
})
})
}
fn arb_long(group_count: usize) -> impl Strategy<Value = TestNote> {
(
-600000..600000i32,
0..1200000i32,
-10.0..10.0f32,
0..=group_count,
)
.prop_map(move |(start, len, pos, gi)| {
TestNote::Long(TestLong {
time: Interval::try_new(
Time::Clock(ClockTime::from_seconds(start as f64 / 1024.0).unwrap()),
Time::Clock(ClockTime::from_seconds((start + len) as f64 / 1024.0).unwrap()),
)
.expect("test endpoints use the same time unit"),
gr_id: group_id_from_index(gi),
pos,
})
})
}
fn arb_note(group_count: usize) -> impl Strategy<Value = TestNote> {
prop_oneof![arb_short(group_count), arb_long(group_count),]
}
fn note_id_parts(id: TestNoteId) -> (u64, TestNoteType) {
match id {
TestNoteId::Short(id) => (id, TestNoteType::Short),
TestNoteId::Long(id) => (id, TestNoteType::Long),
}
}
fn with_id_parts(note: TestNoteWithId) -> (u64, TestNote) {
match note {
TestNoteWithId::Short(id, n) => (id, TestNote::Short(n)),
TestNoteWithId::Long(id, n) => (id, TestNote::Long(n)),
}
}
fn arb_note_from_ty(ty: TestNoteType, group_count: usize) -> BoxedStrategy<TestNote> {
match ty {
TestNoteType::Short => arb_short(group_count).boxed(),
TestNoteType::Long => arb_long(group_count).boxed(),
}
}
fn combine_id_with_note(id: TestNoteId, note: TestNote) -> TestNoteWithId {
match (id, note) {
(TestNoteId::Short(i), TestNote::Short(n)) => TestNoteWithId::Short(i, n),
(TestNoteId::Long(i), TestNote::Long(n)) => TestNoteWithId::Long(i, n),
_ => panic!("Wrong note type generated for id type"),
}
}
fn get_all_notes_id(storage: &TestNotesStorage) -> Vec<TestNoteId> {
storage
.shorts
.note_id()
.map(TestNoteId::Short)
.chain(storage.longs.note_id().map(TestNoteId::Long))
.collect()
}
fn arb_notes(group_count: usize, min: usize, max: usize) -> impl Strategy<Value = Vec<TestNote>> {
prop::collection::vec(arb_note(group_count), min..=max)
}
fn arb_world(
max_named_groups: usize,
min_notes: usize,
max_notes: usize,
) -> impl Strategy<Value = (World, Vec<TestNote>)> {
(0..=max_named_groups).prop_flat_map(move |named_count| {
arb_notes(named_count, min_notes, max_notes)
.prop_map(move |notes| (World::new(named_count), notes))
})
}
fn arb_starting_notes_storage_world(
max_named_groups: usize,
min_notes: usize,
max_notes: usize,
) -> impl Strategy<Value = (World, TestNotesStorage)> {
arb_world(max_named_groups, min_notes, max_notes).prop_map(|(mut world, notes)| {
let storage = world.build_notes_storage(notes);
(world, storage)
})
}
fn arb_starting_notes_storage(
max_named_groups: usize,
min_notes: usize,
max_notes: usize,
) -> impl Strategy<Value = TestNotesStorage> {
arb_starting_notes_storage_world(max_named_groups, min_notes, max_notes)
.prop_map(|(_, storage)| storage)
}
fn arb_starting_notes_storage_select(
max_named_groups: usize,
max_notes: usize,
) -> impl Strategy<Value = (TestNotesStorage, TestNoteId)> {
arb_starting_notes_storage(max_named_groups, 1, max_notes).prop_flat_map(|storage| {
let note_ids = get_all_notes_id(&storage);
let len = note_ids.len();
(Just(storage), (0..len).prop_map(move |i| note_ids[i]))
})
}
fn arb_starting_notes_storage_select_multi(
max_named_groups: usize,
max_notes: usize,
max_select_len: usize,
) -> impl Strategy<Value = (TestNotesStorage, Vec<TestNoteId>)> {
arb_starting_notes_storage(max_named_groups, 1, max_notes).prop_flat_map(move |storage| {
let note_ids = get_all_notes_id(&storage);
let len = note_ids.len();
(
Just(storage),
proptest::collection::hash_set(
(0..len).prop_map(move |i| note_ids[i]),
0..=max_select_len.min(len),
)
.prop_map(|ids| ids.into_iter().collect::<Vec<_>>()),
)
})
}
fn arb_starting_notes_storage_select_note(
max_named_groups: usize,
max_notes: usize,
) -> impl Strategy<Value = (World, TestNotesStorage, TestNoteWithId)> {
arb_starting_notes_storage_world(max_named_groups, 1, max_notes).prop_flat_map(
|(world, storage)| {
let note_ids = get_all_notes_id(&storage);
let len = note_ids.len();
let group_count = world.named_count;
(
Just(world),
Just(storage),
(0..len).prop_flat_map(move |i| {
let id = note_ids[i];
let ty = note_id_parts(id).1;
(Just(id), arb_note_from_ty(ty, group_count))
.prop_map(|(id, note)| combine_id_with_note(id, note))
}),
)
},
)
}
#[derive(Debug, Clone)]
struct Wrap(TestNoteWithId);
impl Wrap {
fn id(&self) -> TestNoteId {
match &self.0 {
TestNoteWithId::Short(id, _) => TestNoteId::Short(*id),
TestNoteWithId::Long(id, _) => TestNoteId::Long(*id),
}
}
}
impl std::hash::Hash for Wrap {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.id().hash(state);
}
}
impl PartialEq for Wrap {
fn eq(&self, other: &Self) -> bool {
self.id() == other.id()
}
}
impl Eq for Wrap {}
fn arb_starting_notes_storage_select_multi_notes(
max_named_groups: usize,
max_notes: usize,
max_select_len: usize,
) -> impl Strategy<Value = (World, TestNotesStorage, Vec<TestNoteWithId>)> {
arb_starting_notes_storage_world(max_named_groups, 1, max_notes).prop_flat_map(
move |(world, storage)| {
let note_ids = get_all_notes_id(&storage);
let len = note_ids.len();
let group_count = world.named_count;
(
Just(world),
Just(storage),
proptest::collection::hash_set(
(0..len).prop_flat_map(move |i| {
let id = note_ids[i];
let ty = note_id_parts(id).1;
(Just(id), arb_note_from_ty(ty, group_count))
.prop_map(|(id, note)| Wrap(combine_id_with_note(id, note)))
}),
0..=max_select_len.min(len),
)
.prop_map(|wraps| wraps.into_iter().map(|w| w.0).collect::<Vec<_>>()),
)
},
)
}
fn expected_notes(notes: &[TestNote]) -> (HashMap<u64, TestShort>, HashMap<u64, TestLong>) {
let mut shorts = HashMap::new();
let mut longs = HashMap::new();
for note in notes {
match note {
TestNote::Short(n) => {
shorts.insert(shorts.len() as u64 + 1, n.clone());
}
TestNote::Long(n) => {
longs.insert(longs.len() as u64 + 1, n.clone());
}
}
}
(shorts, longs)
}
fn arb_starting_notes_storage_with_notes(
max_named_groups: usize,
min_notes: usize,
max_notes: usize,
) -> impl Strategy<Value = (TestNotesStorage, Vec<TestNote>)> {
arb_world(max_named_groups, min_notes, max_notes).prop_map(|(mut world, notes)| {
let storage = world.build_notes_storage(notes.clone());
(storage, notes)
})
}
#[test]
fn add_note_to_empty_stays_consistent() {
let mut world = World::new(0);
let mut storage = TestNotesStorage::new();
world.add_note(
&mut storage,
TestNote::Short(TestShort {
time: Time::Clock(ClockTime::ZERO),
gr_id: GroupId::Main,
pos: 0.0,
}),
);
storage.health_check();
}
#[test]
fn with_notes_stays_consistent() {
let mut world = World::new(1);
let storage = world.build_notes_storage(vec![
TestNote::Short(TestShort {
time: Time::Clock(ClockTime::ZERO),
gr_id: GroupId::Main,
pos: 0.0,
}),
TestNote::Long(TestLong {
time: Interval::try_new(
Time::Clock(ClockTime::ZERO),
Time::Clock(ClockTime::from_milis(1000.0).unwrap()),
)
.expect("test endpoints use the same time unit"),
gr_id: GroupId::Named(0),
pos: 1.0,
}),
]);
storage.health_check();
}
proptest! {
#[test]
fn arb_add_note_stays_consistent(
(mut world, mut storage, adding_note) in arb_starting_notes_storage_select_note(10, 100),
) {
world.add_note(&mut storage, adding_note.into());
storage.health_check();
}
}
proptest! {
#[test]
fn arb_add_multi_notes_stays_consistent(
(mut world, mut storage, adding_notes) in
arb_starting_notes_storage_select_multi_notes(10, 100, 100),
) {
world.add_notes(&mut storage, adding_notes.into_iter().map(TestNote::from).collect());
storage.health_check();
}
}
proptest! {
#[test]
fn arb_remove_note_stays_consistent(
(mut storage, id) in arb_starting_notes_storage_select(10, 100),
) {
let (id, ty) = note_id_parts(id);
storage.remove_note(ty, id);
storage.health_check();
}
}
proptest! {
#[test]
fn arb_remove_multi_notes_stays_consistent(
(mut storage, ids) in arb_starting_notes_storage_select_multi(10, 100, 100),
) {
storage.remove_notes(ids.into_iter().map(note_id_parts).collect());
storage.health_check();
}
}
proptest! {
#[test]
fn arb_replace_note_stays_consistent(
(mut world, mut storage, replacing) in arb_starting_notes_storage_select_note(10, 100),
) {
world.replace_note(&mut storage, replacing);
storage.health_check();
}
}
proptest! {
#[test]
fn arb_replace_multi_notes_stays_consistent(
(mut world, mut storage, replacing) in
arb_starting_notes_storage_select_multi_notes(10, 100, 100),
) {
world.replace_notes(&mut storage, replacing);
storage.health_check();
}
}
proptest! {
#[test]
fn arb_query_notes_returns_originals(
(storage, notes) in arb_starting_notes_storage_with_notes(10, 0, 100),
) {
let (expected_shorts, expected_longs) = expected_notes(¬es);
let all_ids = get_all_notes_id(&storage);
let queried: Vec<_> = storage
.query_notes(all_ids.iter().copied().map(note_id_parts))
.collect();
assert_eq!(queried.len(), all_ids.len());
for snapshot in queried {
assert_eq!(snapshot.clock_time_interval(), Interval::new(clock_of(&snapshot.start_time()), clock_of(&snapshot.start_time()) + clock_of(&snapshot.duration())));
let id = snapshot.id();
let note = snapshot.into_owned_note();
match note {
TestNote::Short(n) => assert_eq!(expected_shorts.get(&id), Some(&n)),
TestNote::Long(n) => assert_eq!(expected_longs.get(&id), Some(&n)),
}
}
let swapped: Vec<(u64, TestNoteType)> = all_ids
.iter()
.copied()
.map(|id| match note_id_parts(id) {
(id, TestNoteType::Short) => (id, TestNoteType::Long),
(id, TestNoteType::Long) => (id, TestNoteType::Short),
})
.collect();
for snapshot in storage.query_notes(swapped.into_iter()) {
let id = snapshot.id();
let note = snapshot.into_owned_note();
match note {
TestNote::Short(n) => assert_eq!(expected_shorts.get(&id), Some(&n)),
TestNote::Long(n) => assert_eq!(expected_longs.get(&id), Some(&n)),
}
}
let invalid = [(0, TestNoteType::Short), (0, TestNoteType::Long)];
assert_eq!(storage.query_notes(invalid.into_iter()).count(), 0);
assert_eq!(get_all_notes_id(&storage), all_ids);
storage.health_check();
}
}
fn arb_clock_time() -> impl Strategy<Value = ClockTime> {
(-600000..600000i32).prop_map(|t| ClockTime::from_milis(t as f64).unwrap())
}
fn clock_of(time: &Time) -> ClockTime {
match time {
Time::Clock(c) => *c,
Time::Beat(_) => panic!("test notes should be in clock time"),
}
}
fn clock_interval_of(time: &Interval<Time>) -> Interval<ClockTime> {
Interval::new(clock_of(&time.start()), clock_of(&time.end()))
}
fn note_sort_key(id: u64, note: &TestNote) -> (u64, u8) {
(
id,
match note {
TestNote::Short(_) => 0,
TestNote::Long(_) => 1,
},
)
}
fn naive_overlapping_ids(
expected: &(HashMap<u64, TestShort>, HashMap<u64, TestLong>),
range: Interval<ClockTime>,
) -> Vec<(u64, TestNote)> {
let mut naive = vec![];
for (id, n) in expected.0.iter() {
if range.contains(&clock_of(&n.time)) {
naive.push((*id, TestNote::Short(n.clone())));
}
}
for (id, n) in expected.1.iter() {
if clock_interval_of(&n.time).intersect(&range) {
naive.push((*id, TestNote::Long(n.clone())));
}
}
naive.sort_by_key(|(id, n)| note_sort_key(*id, n));
naive
}
proptest! {
#[test]
fn arb_query_notes_state_returns_originals(
(storage, notes) in arb_starting_notes_storage_with_notes(10, 0, 100),
) {
let expected = expected_notes(¬es);
let all_ids = get_all_notes_id(&storage);
let states: Vec<_> = storage
.query_notes_ref(all_ids.iter().copied().map(note_id_parts))
.collect();
assert_eq!(states.len(), all_ids.len());
for state in states {
match state {
TestNoteRef::Short(s) => {
let n = expected.0.get(&s.id).expect("id should be valid");
let clock_time = clock_of(&n.time);
assert_eq!(s.start_time, n.time);
assert_eq!(s.duration, Time::Clock(ClockTime::ZERO));
assert_eq!(s.clock_time.start(), clock_time);
assert_eq!(s.clock_time.end(), clock_time);
assert_eq!(&s.group_id, &n.gr_id);
assert_eq!(*s.value_data.0, n.pos);
assert_eq!(*s.runtime_data.0, 0.0);
}
TestNoteRef::Long(s) => {
let n = expected.1.get(&s.id).expect("id should be valid");
let clock_time = clock_interval_of(&n.time);
assert_eq!(s.start_time, n.time.start());
assert_eq!(s.duration, crate::notes::LongNoteData::duration(n));
assert_eq!(s.clock_time.start(), clock_time.start());
assert_eq!(s.clock_time.end(), clock_time.end());
assert_eq!(&s.group_id, &n.gr_id);
assert_eq!(*s.value_data.0, n.pos);
assert_eq!(*s.runtime_data.0, 0.0);
}
}
}
let invalid = [(0, TestNoteType::Short), (0, TestNoteType::Long)];
assert_eq!(storage.query_notes_ref(invalid.into_iter()).count(), 0);
storage.health_check();
}
}
proptest! {
#[test]
fn arb_notes_overlapping_range_matches_naive(
(storage, notes) in arb_starting_notes_storage_with_notes(10, 0, 100),
(t1, t2) in (arb_clock_time(), arb_clock_time()),
) {
let expected = expected_notes(¬es);
let range = Interval::new(t1, t2);
let mut overlapping: Vec<(u64, TestNote)> =
storage.notes_overlapping_range(range).map(|snapshot| {
assert_eq!(snapshot.clock_time_interval(), Interval::new(clock_of(&snapshot.start_time()), clock_of(&snapshot.start_time()) + clock_of(&snapshot.duration())));
(snapshot.id(), snapshot.into_owned_note())
}).collect();
overlapping.sort_by_key(|(id, n)| note_sort_key(*id, n));
assert_eq!(overlapping, naive_overlapping_ids(&expected, range));
storage.health_check();
}
}
proptest! {
#[test]
fn arb_notes_state_overlapping_range_matches_naive(
(storage, notes) in arb_starting_notes_storage_with_notes(10, 0, 100),
(t1, t2) in (arb_clock_time(), arb_clock_time()),
) {
let expected = expected_notes(¬es);
let range = Interval::new(t1, t2);
let states: Vec<_> = storage.notes_ref_overlapping_range(range).collect();
let naive = naive_overlapping_ids(&expected, range);
assert_eq!(states.len(), naive.len());
for state in states {
match state {
TestNoteRef::Short(s) => {
let n = expected.0.get(&s.id).expect("id should be valid");
assert!(range.contains(&clock_of(&n.time)));
assert_eq!(s.start_time, n.time);
assert_eq!(s.duration, Time::Clock(ClockTime::ZERO));
assert_eq!(*s.value_data.0, n.pos);
}
TestNoteRef::Long(s) => {
let n = expected.1.get(&s.id).expect("id should be valid");
assert!(clock_interval_of(&n.time).intersect(&range));
assert_eq!(s.start_time, n.time.start());
assert_eq!(s.duration, crate::notes::LongNoteData::duration(n));
assert_eq!(*s.value_data.0, n.pos);
}
}
}
storage.health_check();
}
}
proptest! {
#[test]
fn arb_query_notes_state_mut_mutates_data(
(mut storage, ids) in arb_starting_notes_storage_select_multi(10, 100, 100),
) {
let mut visited = 0;
storage.query_notes_mut(
ids.iter().copied().map(note_id_parts)
.chain(ids.iter().copied().map(note_id_parts)),
|state| {
visited += 1;
match state {
TestNoteMut::Short(s) => {
*s.runtime_data.0 = 7.0;
}
TestNoteMut::Long(s) => {
*s.runtime_data.0 = 7.0;
}
}
},
);
assert_eq!(visited, ids.len() * 2);
for id in ids.iter().copied() {
let (id, ty) = note_id_parts(id);
let mut states = storage.query_notes_ref(std::iter::once((id, ty)));
let state = states.next().expect("id should be valid");
match state {
TestNoteRef::Short(s) => {
assert_eq!(*s.runtime_data.0, 7.0);
}
TestNoteRef::Long(s) => {
assert_eq!(*s.runtime_data.0, 7.0);
}
}
}
storage.health_check();
}
}
proptest! {
#[test]
fn arb_notes_state_mut_overlapping_range_mutates_data(
(mut storage, notes) in arb_starting_notes_storage_with_notes(10, 0, 100),
(t1, t2) in (arb_clock_time(), arb_clock_time()),
) {
let expected = expected_notes(¬es);
let range = Interval::new(t1, t2);
let mut visited = 0;
storage.notes_mut_overlapping_range(range, |state| {
visited += 1;
match state {
TestNoteMut::Short(s) => {
*s.runtime_data.0 = 9.0;
}
TestNoteMut::Long(s) => {
*s.runtime_data.0 = 9.0;
}
}
});
assert_eq!(visited, naive_overlapping_ids(&expected, range).len());
for state in storage.notes_ref_overlapping_range(range) {
match state {
TestNoteRef::Short(s) => assert_eq!(*s.runtime_data.0, 9.0),
TestNoteRef::Long(s) => assert_eq!(*s.runtime_data.0, 9.0),
}
}
storage.health_check();
}
}
fn arb_starting_notes_storage_extract_multi(
max_named_groups: usize,
max_notes: usize,
max_select_len: usize,
) -> impl Strategy<Value = (TestNotesStorage, Vec<TestNote>, Vec<TestNoteId>)> {
arb_starting_notes_storage_with_notes(max_named_groups, 1, max_notes).prop_flat_map(
move |(storage, notes)| {
let note_ids = get_all_notes_id(&storage);
let len = note_ids.len();
(
Just(storage),
Just(notes),
proptest::collection::hash_set(
(0..len).prop_map(move |i| note_ids[i]),
0..=max_select_len.min(len),
)
.prop_map(|ids| ids.into_iter().collect::<Vec<_>>()),
)
},
)
}
proptest! {
#[test]
fn arb_extract_notes_returns_originals_and_removes(
(mut storage, notes, ids) in arb_starting_notes_storage_extract_multi(10, 100, 100),
) {
let (expected_shorts, expected_longs) = expected_notes(¬es);
let all_ids = get_all_notes_id(&storage);
let extract_ids: Vec<(u64, TestNoteType)> =
ids.iter().copied().map(note_id_parts).collect();
let mut extracted = vec![];
storage.extract_notes(
extract_ids
.iter()
.copied()
.chain([(0, TestNoteType::Short)])
.collect(),
|snapshot| {
assert_eq!(snapshot.clock_time_interval(), Interval::new(clock_of(&snapshot.start_time()), clock_of(&snapshot.start_time()) + clock_of(&snapshot.duration())));
extracted.push((snapshot.id(), snapshot.into_owned_note()));
},
);
assert_eq!(extracted.len(), extract_ids.len());
for (id, note) in extracted {
match note {
TestNote::Short(n) => assert_eq!(expected_shorts.get(&id), Some(&n)),
TestNote::Long(n) => assert_eq!(expected_longs.get(&id), Some(&n)),
}
}
let remaining = get_all_notes_id(&storage);
assert_eq!(remaining.len(), all_ids.len() - extract_ids.len());
for (id, ty) in extract_ids {
let tagged = match ty {
TestNoteType::Short => TestNoteId::Short(id),
TestNoteType::Long => TestNoteId::Long(id),
};
assert!(!remaining.contains(&tagged));
}
storage.health_check();
}
}
fn short_note(pos: f32) -> TestNote {
TestNote::Short(short_data(pos))
}
fn long_note(pos: f32) -> TestNote {
TestNote::Long(long_data(pos))
}
#[test]
fn add_note_fires_hooks() {
let mut world = World::new(0);
let mut storage = TestNotesStorage::new();
world.add_note(&mut storage, short_note(0.0));
assert_eq!(
storage.hook_log,
vec![
HookEvent::PreAdd(short_note(0.0)),
HookEvent::PostAdd(TestNoteType::Short, 1),
]
);
}
#[test]
fn with_notes_fires_hooks_in_batch_order() {
let mut world = World::new(0);
let notes = vec![short_note(0.0), long_note(1.0)];
let storage = world.build_notes_storage(notes.clone());
assert_eq!(
storage.hook_log,
vec![
HookEvent::PreAdd(notes[0].clone()),
HookEvent::PreAdd(notes[1].clone()),
HookEvent::PostAdd(TestNoteType::Short, 1),
HookEvent::PostAdd(TestNoteType::Long, 1),
]
);
assert_eq!(storage.hello, 2.0);
}
#[test]
fn remove_note_fires_hooks() {
let mut world = World::new(0);
let mut storage = world.build_notes_storage(vec![short_note(0.0)]);
storage.hook_log.clear();
storage.remove_note(TestNoteType::Short, 1);
assert_eq!(
storage.hook_log,
vec![
HookEvent::PreRemove(TestNoteType::Short, 1),
HookEvent::PostRemove(short_note(0.0), 1),
]
);
}
#[test]
fn remove_note_with_missing_or_mistyped_id_does_not_fire_hooks() {
let mut world = World::new(0);
let mut storage = world.build_notes_storage(vec![short_note(0.0)]);
storage.hook_log.clear();
storage.remove_note(TestNoteType::Short, 999);
storage.remove_note(TestNoteType::Long, 1);
assert!(storage.hook_log.is_empty());
}
#[test]
fn remove_notes_fires_hooks_once_per_removed_note() {
let mut world = World::new(0);
let mut storage =
world.build_notes_storage(vec![short_note(0.0), short_note(1.0), long_note(2.0)]);
storage.hook_log.clear();
storage.remove_notes(vec![
(1, TestNoteType::Short),
(1, TestNoteType::Short),
(999, TestNoteType::Short),
]);
assert_eq!(
storage.hook_log,
vec![
HookEvent::PreRemove(TestNoteType::Short, 1),
HookEvent::PostRemove(short_note(0.0), 1),
]
);
}
#[test]
fn replace_note_fires_hooks() {
let mut world = World::new(0);
let mut storage = world.build_notes_storage(vec![short_note(0.0)]);
storage.hook_log.clear();
world.replace_note(&mut storage, TestNoteWithId::Short(1, short_data(5.0)));
assert_eq!(
storage.hook_log,
vec![
HookEvent::PreUpdate(1, short_note(5.0)),
HookEvent::PostUpdate(1, short_note(0.0), short_note(5.0)),
]
);
}
#[test]
fn replace_note_with_missing_or_mistyped_id_does_not_fire_hooks() {
let mut world = World::new(0);
let mut storage = world.build_notes_storage(vec![short_note(0.0)]);
storage.hook_log.clear();
world.replace_note(&mut storage, TestNoteWithId::Short(999, short_data(5.0)));
world.replace_note(&mut storage, TestNoteWithId::Long(1, long_data(5.0)));
assert!(storage.hook_log.is_empty());
}
#[test]
fn replace_notes_last_pair_wins_and_missing_ids_are_skipped() {
let mut world = World::new(0);
let mut storage = world.build_notes_storage(vec![short_note(0.0), long_note(1.0)]);
storage.hook_log.clear();
world.replace_notes(
&mut storage,
vec![
TestNoteWithId::Short(1, short_data(3.0)),
TestNoteWithId::Short(1, short_data(7.0)),
TestNoteWithId::Short(999, short_data(9.0)),
],
);
assert_eq!(
storage.hook_log,
vec![
HookEvent::PreUpdate(1, short_note(7.0)),
HookEvent::PostUpdate(1, short_note(0.0), short_note(7.0)),
]
);
}
#[test]
fn extract_notes_fires_hooks() {
let mut world = World::new(0);
let mut storage = world.build_notes_storage(vec![short_note(0.0)]);
storage.hook_log.clear();
let mut extracted = vec![];
storage.extract_notes(vec![(1, TestNoteType::Short)], |snapshot| {
extracted.push((snapshot.id(), snapshot.into_owned_note()))
});
assert_eq!(extracted, vec![(1, short_note(0.0))]);
assert_eq!(
storage.hook_log,
vec![
HookEvent::PreRemove(TestNoteType::Short, 1),
HookEvent::PostRemove(short_note(0.0), 1),
]
);
}
#[test]
fn mixed_type_batches_expose_complete_storage_at_hook_boundaries() {
for extract in [false, true] {
let mut world = World::new(0);
let mut storage = world.build_notes_storage(vec![short_note(0.0), long_note(1.0)]);
assert_eq!(storage.hook_sizes, vec![2, 2]);
assert_eq!(storage.hook_batches, vec![2, 2]);
storage.hook_batches.clear();
storage.hook_sizes.clear();
storage.hook_log.clear();
let ids = vec![
(1, TestNoteType::Long),
(1, TestNoteType::Short),
(1, TestNoteType::Long),
(999, TestNoteType::Short),
];
let mut extracted = Vec::new();
if extract {
storage.extract_notes(ids, |note| extracted.push(note.into_owned_note()));
assert_eq!(extracted, vec![short_note(0.0), long_note(1.0)]);
} else {
storage.remove_notes(ids);
}
assert_eq!(storage.hook_sizes, vec![2, 2, 0, 0]);
assert_eq!(storage.hook_batches, vec![2, 2]);
assert_eq!(
storage.hook_log,
vec![
HookEvent::PreRemove(TestNoteType::Short, 1),
HookEvent::PreRemove(TestNoteType::Long, 1),
HookEvent::PostRemove(short_note(0.0), 1),
HookEvent::PostRemove(long_note(1.0), 1),
]
);
}
}
#[test]
fn replace_notes_distinguishes_types_and_keeps_last_value_per_identity() {
let mut world = World::new(0);
let mut storage =
world.build_notes_storage(vec![short_note(0.0), short_note(1.0), long_note(2.0)]);
storage.hook_log.clear();
storage.hook_batches.clear();
world.replace_notes(
&mut storage,
vec![
TestNoteWithId::Long(1, long_data(10.0)),
TestNoteWithId::Short(2, short_data(20.0)),
TestNoteWithId::Short(1, short_data(30.0)),
TestNoteWithId::Long(1, long_data(40.0)),
TestNoteWithId::Short(1, short_data(50.0)),
TestNoteWithId::Long(2, long_data(60.0)),
],
);
assert_eq!(storage.hook_batches, vec![3, 3]);
assert_eq!(
storage.hook_log,
vec![
HookEvent::PreUpdate(1, short_note(50.0)),
HookEvent::PreUpdate(2, short_note(20.0)),
HookEvent::PreUpdate(1, long_note(40.0)),
HookEvent::PostUpdate(1, short_note(0.0), short_note(50.0)),
HookEvent::PostUpdate(2, short_note(1.0), short_note(20.0)),
HookEvent::PostUpdate(1, long_note(2.0), long_note(40.0)),
]
);
let stored: Vec<_> = storage
.query_notes(
[
(1, TestNoteType::Short),
(2, TestNoteType::Short),
(1, TestNoteType::Long),
]
.into_iter(),
)
.map(|note| note.into_owned_note())
.collect();
assert_eq!(
stored,
vec![short_note(50.0), short_note(20.0), long_note(40.0)]
);
storage.health_check();
}
#[test]
fn implements_hook_trait_for_3_hooks() {
let hook1: SameStartTime<TestNotesStorage> = SameStartTime::new();
let hook2: ParentChild<TestNotesStorage> =
ParentChild::new(TestNoteType::Long, TestNoteType::Short, |_| None);
let hook3: ChainForward<TestNotesStorage> =
ChainForward::new(ClockTime::from_milis(10.).unwrap(), &[]);
let mut s = TestNotesStorage::new();
NotesStorageEditHook::post_rebuild_clock_time(&mut (hook1, (hook2, hook3)), &mut s);
}