use proptest::prelude::*;
use crate::collections::IntervalTree;
use crate::math::Easing;
use crate::rhythm::{
ScrollSpeedChangeEvent, ScrollSpeedTrack, Tempo, TempoChangeEvent, TempoTrack,
};
use super::*;
use crate::notes::{NoteGroup, NoteGroups};
use crate::rhythm::{BeatTime, ScrollPosition, ScrollSpeedTracks, TempoTracks};
#[derive(Debug, Clone, PartialEq)]
pub struct TestNote {
time: Interval<Time>,
group_id: GroupId,
}
#[derive(Debug, Soars, SoaClone, Clone, Copy)]
pub struct TestNoteValue {
is_event_track_applied: bool,
}
#[derive(Debug, Soars, SoaClone, Clone, Copy, Default)]
pub struct TestNoteRuntime {
is_event_track_applied: u32,
event_track_calls: u32,
}
impl LongNoteData for TestNote {
type ValueData = TestNoteValue;
type RuntimeData = TestNoteRuntime;
fn start_time(&self) -> Time {
self.time.start()
}
fn duration(&self) -> Time {
generativity::make_guard!(guard);
let (start, end) = self
.time
.start()
.prove_same_unit(self.time.end(), guard)
.expect("test interval endpoints use the same time unit");
(end - start).as_time()
}
fn group_id(&self) -> GroupId {
self.group_id
}
fn value_data(&self) -> Self::ValueData {
TestNoteValue {
is_event_track_applied: true,
}
}
fn apply_event_track(
note: crate::notes::NoteMut<'_, Self>,
_tempo_track: &TempoTrack,
_scroll_track: &ScrollSpeedTrack,
) {
*note.runtime_data.is_event_track_applied = 1;
*note.runtime_data.event_track_calls += 1;
}
fn reconstruct<'a>(
start_time: Time,
duration: Time,
_value_data: <Self::ValueData as soa_rs::Soars>::Ref<'a>,
group_id: GroupId,
) -> Self {
generativity::make_guard!(guard);
let (start, duration) = start_time
.prove_same_unit(duration, guard)
.expect("test note timing uses the same time unit");
let time = Interval::try_new(start_time, (start + duration).as_time()).unwrap();
Self { time, group_id }
}
}
#[derive(Debug, Clone)]
struct TestGroup {
tempo_track_id: TrackId,
scroll_track_id: TrackId,
}
impl NoteGroup for TestGroup {
fn main_group() -> Self {
Self {
tempo_track_id: TrackId::Main,
scroll_track_id: TrackId::Main,
}
}
fn tempo_track_id(&self) -> TrackId {
self.tempo_track_id
}
fn scroll_track_id(&self) -> TrackId {
self.scroll_track_id
}
}
#[derive(Debug, Clone)]
struct World {
named_count: usize,
note_groups: NoteGroups<TestGroup>,
tempo_tracks: TempoTracks,
scroll_tracks: ScrollSpeedTracks,
}
fn test_tempo_track(bpm: f64) -> TempoTrack {
TempoTrack::with_events(vec![TempoChangeEvent {
time: Time::Clock(ClockTime::ZERO),
tempo: Tempo::from_bpm(bpm).unwrap(),
ease: Easing::InConst,
}])
.unwrap()
}
impl World {
fn new(named_count: usize) -> Self {
let tempo_tracks = TempoTracks::with_tracks(
test_tempo_track(60.0),
(0..named_count).map(|i| (i as u64, test_tempo_track(60.0 + 10.0 * i as f64))),
);
let scroll_tracks = ScrollSpeedTracks::with_tracks(
ScrollSpeedTrack::new(TrackId::Main),
(0..named_count).map(|i| {
(
i as u64,
ScrollSpeedTrack::with_events(
TrackId::Named(i as u64),
vec![
ScrollSpeedChangeEvent::new(
Time::Clock(ClockTime::ZERO),
1.0 + i as f64,
Easing::InConst,
)
.unwrap(),
],
&tempo_tracks,
),
)
}),
);
let note_groups = NoteGroups::with_groups(
TestGroup::main_group(),
(0..named_count).map(|i| {
(
i as u64,
TestGroup {
tempo_track_id: TrackId::Named(i as u64),
scroll_track_id: TrackId::Named(i as u64),
},
)
}),
);
Self {
named_count,
note_groups,
tempo_tracks,
scroll_tracks,
}
}
fn build_storage(&mut self, notes: Vec<TestNote>) -> LongNoteTypeStorage<TestNote> {
LongNoteTypeStorage::with_notes(
&self.note_groups,
&self.tempo_tracks,
&self.scroll_tracks,
notes.into_iter(),
)
}
fn add_note(&mut self, storage: &mut LongNoteTypeStorage<TestNote>, note: TestNote) {
storage.add_note(
&self.note_groups,
&self.tempo_tracks,
&self.scroll_tracks,
note,
);
}
fn add_notes(
&mut self,
storage: &mut LongNoteTypeStorage<TestNote>,
notes: impl Iterator<Item = TestNote>,
) {
storage.add_notes(
&self.note_groups,
&self.tempo_tracks,
&self.scroll_tracks,
notes,
);
}
fn replace_note(
&mut self,
storage: &mut LongNoteTypeStorage<TestNote>,
id: u64,
note: TestNote,
) {
storage.replace_note(
&self.note_groups,
&self.tempo_tracks,
&self.scroll_tracks,
id,
note,
);
}
fn replace_notes(
&mut self,
storage: &mut LongNoteTypeStorage<TestNote>,
replaces: impl Iterator<Item = (u64, TestNote)>,
) {
storage.replace_notes(
&self.note_groups,
&self.tempo_tracks,
&self.scroll_tracks,
replaces,
);
}
}
fn group_id_from_index(i: usize) -> GroupId {
if i == 0 {
GroupId::Main
} else {
GroupId::Named(i as u64 - 1)
}
}
fn arb_time() -> impl Strategy<Value = Interval<Time>> {
(-600000..600000i32, 0..1200000i32).prop_map(|(start_ms, length_ms)| {
Interval::try_new(
Time::Clock(ClockTime::from_seconds(start_ms as f64 / 1024.0).unwrap()),
Time::Clock(ClockTime::from_seconds((start_ms + length_ms) as f64 / 1024.0).unwrap()),
)
.expect("test endpoints use the same time unit")
})
}
fn arb_note(group_count: usize) -> impl Strategy<Value = TestNote> {
(arb_time(), 0..=group_count).prop_map(move |(time, group_i)| TestNote {
time,
group_id: group_id_from_index(group_i),
})
}
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_data(
max_named_groups: usize,
max_notes: usize,
) -> impl Strategy<Value = (World, Vec<TestNote>)> {
arb_world(max_named_groups, 0, max_notes)
}
fn arb_starting_data_select(
max_named_groups: usize,
max_notes: usize,
) -> impl Strategy<Value = (World, Vec<TestNote>, usize)> {
arb_world(max_named_groups, 1, max_notes).prop_flat_map(|(world, notes)| {
let len = notes.len();
(Just(world), Just(notes), 0..len)
})
}
fn arb_starting_data_select_note(
max_named_groups: usize,
max_notes: usize,
) -> impl Strategy<Value = (World, Vec<TestNote>, usize, TestNote)> {
arb_starting_data_select(max_named_groups, max_notes).prop_flat_map(
move |(world, notes, index)| {
let group_count = world.named_count;
(Just(world), Just(notes), Just(index), arb_note(group_count))
},
)
}
fn arb_starting_data_select_multi(
max_named_groups: usize,
max_notes: usize,
max_select_len: usize,
) -> impl Strategy<Value = (World, Vec<TestNote>, Vec<usize>)> {
arb_world(max_named_groups, 1, max_notes).prop_flat_map(move |(world, notes)| {
let len = notes.len();
(
Just(world),
Just(notes),
proptest::collection::hash_set(0..len, 0..=max_select_len.min(len))
.prop_map(|x| x.into_iter().collect::<Vec<_>>()),
)
})
}
fn arb_starting_data_select_multi_notes(
max_named_groups: usize,
max_notes: usize,
max_select_len: usize,
) -> impl Strategy<Value = (World, Vec<TestNote>, Vec<usize>, Vec<TestNote>)> {
arb_starting_data_select_multi(max_named_groups, max_notes, max_select_len).prop_flat_map(
move |(world, notes, indices)| {
let len = indices.len();
let group_count = world.named_count;
(
Just(world),
Just(notes),
Just(indices),
proptest::collection::vec(arb_note(group_count), len),
)
},
)
}
fn arb_starting_data_select_group(
max_named_groups: usize,
max_notes: usize,
) -> impl Strategy<Value = (World, Vec<TestNote>, GroupId)> {
arb_world(max_named_groups, 1, max_notes).prop_flat_map(move |(world, notes)| {
let group_count = world.named_count;
(
Just(world),
Just(notes),
(0..=group_count).prop_map(group_id_from_index),
)
})
}
fn test_time(start_ms: f64, end_ms: f64) -> Interval<Time> {
Interval::try_new(
Time::Clock(ClockTime::from_milis(start_ms).unwrap()),
Time::Clock(ClockTime::from_milis(end_ms).unwrap()),
)
.expect("test endpoints use the same time unit")
}
#[test]
fn add_note_to_empty_stays_consistent() {
let mut world = World::new(0);
let mut storage = LongNoteTypeStorage::new();
world.add_note(
&mut storage,
TestNote {
time: test_time(1.0, 2.0),
group_id: GroupId::Main,
},
);
storage.health_check();
}
#[test]
fn add_note_stays_consistent() {
let mut world = World::new(0);
let mut storage = world.build_storage(vec![TestNote {
time: test_time(1.0, 2.0),
group_id: GroupId::Main,
}]);
world.add_note(
&mut storage,
TestNote {
time: test_time(0.0, 1.0),
group_id: GroupId::Main,
},
);
storage.health_check();
let notes = storage
.query_notes_ref(storage.note_id())
.map(|note| (note.id, *note.runtime_data.is_event_track_applied))
.collect::<std::collections::HashMap<_, _>>();
assert_eq!(notes[&1], 1);
assert_eq!(notes[&2], 1);
assert_event_track_calls(&storage, &[(1, 1), (2, 1)]);
}
proptest! {
#[test]
fn arb_add_note_stays_consistent(
(mut world, notes, _, adding_note) in arb_starting_data_select_note(10, 100),
) {
let mut storage = world.build_storage(notes);
world.add_note(&mut storage, adding_note);
storage.health_check();
}
}
proptest! {
#[test]
fn arb_add_multi_notes_stays_consistent(
(mut world, notes, _, adding_notes) in
arb_starting_data_select_multi_notes(10, 100, 100),
) {
let mut storage = world.build_storage(notes);
world.add_notes(&mut storage, adding_notes.into_iter());
storage.health_check();
}
}
proptest! {
#[test]
fn arb_remove_note_stays_consistent(
(mut world, notes, index) in arb_starting_data_select(10, 100),
) {
let mut storage = world.build_storage(notes);
let note_id = storage.note_id().nth(index).unwrap();
storage.remove_note(note_id);
storage.health_check();
}
}
proptest! {
#[test]
fn arb_remove_multi_notes_stays_consistent(
(mut world, notes, indices) in arb_starting_data_select_multi(10, 100, 100),
) {
let mut storage = world.build_storage(notes);
let note_ids = indices.into_iter().map(|i| storage.note_id().nth(i).unwrap()).collect::<Vec<_>>();
storage.remove_notes(note_ids.into_iter());
storage.health_check();
}
}
proptest! {
#[test]
fn arb_replace_note_stays_consistent(
(mut world, notes, index, replace_with) in arb_starting_data_select_note(10, 100),
) {
let mut storage = world.build_storage(notes);
let note_id = storage.note_id().nth(index).unwrap();
world.replace_note(&mut storage, note_id, replace_with);
let mut expected: Vec<_> = storage.note_id()
.map(|id| (id, if id == note_id { 2 } else { 1 }))
.collect();
expected.sort_unstable();
assert_event_track_calls(&storage, &expected);
storage.health_check();
assert_eq!(*storage.query_notes_ref(std::iter::once(note_id)).next().unwrap().runtime_data.is_event_track_applied, 1);
}
}
proptest! {
#[test]
fn arb_replace_multi_notes_stays_consistent(
(mut world, notes, indices, replace_with) in
arb_starting_data_select_multi_notes(10, 100, 100),
) {
let mut storage = world.build_storage(notes);
let note_ids = indices.into_iter().map(|i| storage.note_id().nth(i).unwrap()).collect::<Vec<_>>();
world.replace_notes(&mut storage, std::iter::zip(note_ids, replace_with));
storage.health_check();
}
}
proptest! {
#[test]
fn arb_remove_group_stays_consistent(
(mut world, notes, removing_group_id) in arb_starting_data_select_group(10, 100),
) {
let mut storage = world.build_storage(notes);
storage.remove_group(&removing_group_id, |_| {});
storage.health_check();
}
}
proptest! {
#[test]
fn arb_empty_groups_are_removed(
(mut world, notes) in arb_starting_data(10, 100),
) {
let mut storage = world.build_storage(notes);
let note_ids = storage.note_id().collect::<Vec<_>>();
storage.remove_notes(note_ids.into_iter());
assert!(storage.scroll_sorted_groups().is_empty())
}
}
fn expected_notes(notes: &[TestNote]) -> HashMap<u64, TestNote> {
notes
.iter()
.enumerate()
.map(|(i, note)| (i as u64 + 1, note.clone()))
.collect()
}
proptest! {
#[test]
fn arb_query_notes_returns_originals(
(mut world, notes) in arb_starting_data(10, 100),
) {
let storage = world.build_storage(notes.clone());
let expected = expected_notes(¬es);
let all_ids = storage.note_id().collect::<Vec<_>>();
let queried: Vec<(u64, TestNote)> =
storage.query_notes(all_ids.iter().copied()).collect();
assert_eq!(queried.len(), all_ids.len());
for (id, note) in queried {
assert_eq!(expected.get(&id), Some(¬e));
}
let invalid = [0u64, all_ids.len() as u64 + 1];
assert_eq!(storage.query_notes(invalid.iter().copied()).count(), 0);
assert_eq!(storage.note_id().collect::<Vec<_>>(), all_ids);
storage.health_check();
}
}
proptest! {
#[test]
fn arb_extract_notes_returns_originals_and_removes(
(mut world, notes, indices) in arb_starting_data_select_multi(10, 100, 100),
) {
let mut storage = world.build_storage(notes.clone());
let expected = expected_notes(¬es);
let all_ids = storage.note_id().collect::<Vec<_>>();
let extract_ids: Vec<u64> = indices.iter().map(|&i| all_ids[i]).collect();
let mut extracted = vec![];
storage.extract_notes(
extract_ids.iter().copied().chain([0u64]),
|id, note| extracted.push((id, note)),
);
assert_eq!(extracted.len(), extract_ids.len());
for (id, note) in extracted {
assert_eq!(expected.get(&id), Some(¬e));
}
assert_eq!(storage.note_id().count(), all_ids.len() - extract_ids.len());
assert!(extract_ids.iter().all(|id| !storage.note_id().any(|stored| stored == *id)));
storage.health_check();
}
}
fn clock_note(ms: f64, group_id: GroupId) -> TestNote {
TestNote {
time: test_time(ms, ms + 10.0),
group_id,
}
}
fn reversed_scroll_storage() -> (World, LongNoteTypeStorage<TestNote>) {
let mut world = World::new(1);
world.scroll_tracks = ScrollSpeedTracks::with_tracks(
ScrollSpeedTrack::with_events(
TrackId::Main,
vec![
ScrollSpeedChangeEvent::new(Time::Clock(ClockTime::ZERO), -1.0, Easing::InConst)
.unwrap(),
],
&world.tempo_tracks,
),
std::iter::empty(),
);
let storage = world.build_storage(vec![
clock_note(30.0, GroupId::Main),
clock_note(0.0, GroupId::Main),
clock_note(20.0, GroupId::Main),
]);
(world, storage)
}
fn assert_event_track_calls(storage: &LongNoteTypeStorage<TestNote>, expected: &[(u64, u32)]) {
macro_rules! check_groups {
($groups:expr) => {{
let mut actual: Vec<_> = $groups
.iter()
.flat_map(|group| {
group.note_id().iter().copied().zip(
group
.runtime_data()
.iter()
.map(|runtime| *runtime.event_track_calls),
)
})
.collect();
actual.sort_unstable();
assert_eq!(actual, expected);
}};
}
check_groups!(storage.time_sorted_groups());
check_groups!(storage.scroll_sorted_groups());
}
#[test]
fn with_notes_applies_event_track_once_per_index_copy() {
let mut world = World::new(0);
let storage = world.build_storage(vec![
clock_note(20.0, GroupId::Main),
clock_note(0.0, GroupId::Main),
]);
assert_event_track_calls(&storage, &[(1, 1), (2, 1)]);
}
#[test]
fn add_and_replace_apply_event_track_only_to_changed_note_copies() {
let mut world = World::new(0);
let mut storage = world.build_storage(vec![
clock_note(20.0, GroupId::Main),
clock_note(0.0, GroupId::Main),
]);
world.add_note(&mut storage, clock_note(10.0, GroupId::Main));
assert_event_track_calls(&storage, &[(1, 1), (2, 1), (3, 1)]);
world.replace_note(&mut storage, 2, clock_note(30.0, GroupId::Main));
assert_event_track_calls(&storage, &[(1, 1), (2, 2), (3, 1)]);
}
fn assert_runtime_in_both_copies(storage: &LongNoteTypeStorage<TestNote>, expected: &[(u64, u32)]) {
storage.health_check();
{
let groups = storage.time_sorted_groups();
let mut actual: Vec<_> = groups
.iter()
.flat_map(|group| {
group.note_id().iter().enumerate().map(|(i, &id)| {
(
id,
*group.runtime_data().get(i).unwrap().is_event_track_applied,
)
})
})
.collect();
actual.sort_unstable();
assert_eq!(actual, expected);
}
{
let groups = storage.scroll_sorted_groups();
let mut actual: Vec<_> = groups
.iter()
.flat_map(|group| {
group.note_id().iter().enumerate().map(|(i, &id)| {
(
id,
*group.runtime_data().get(i).unwrap().is_event_track_applied,
)
})
})
.collect();
actual.sort_unstable();
assert_eq!(actual, expected);
}
}
#[test]
fn negative_scroll_speed_reverses_note_order() {
let (_, storage) = reversed_scroll_storage();
assert_eq!(storage.time_sorted_groups()[0].note_id(), &[2, 3, 1]);
assert_eq!(storage.scroll_sorted_groups()[0].note_id(), &[1, 3, 2]);
storage.health_check();
}
#[test]
fn runtime_mutation_syncs_copies_once_per_id_visit() {
let (_, mut storage) = reversed_scroll_storage();
let mut visits = vec![];
storage.query_notes_mut([1, 2, 3, 2, 0].into_iter(), |state| {
visits.push(state.id);
*state.runtime_data.is_event_track_applied += state.id as u32;
});
assert_eq!(visits, vec![1, 2, 3, 2]);
assert_runtime_in_both_copies(&storage, &[(1, 2), (2, 5), (3, 4)]);
}
#[test]
fn batch_group_moves_preserve_runtime_and_use_last_replacement() {
let (mut world, mut storage) = reversed_scroll_storage();
storage.query_notes_mut([1, 2, 3].into_iter(), |state| {
*state.runtime_data.is_event_track_applied = state.id as u32;
});
world.replace_notes(
&mut storage,
[
(2, clock_note(50.0, GroupId::Named(0))),
(1, clock_note(60.0, GroupId::Named(0))),
(2, clock_note(70.0, GroupId::Main)),
]
.into_iter(),
);
assert_eq!(
storage.query_notes([1, 2].into_iter()).collect::<Vec<_>>(),
vec![
(1, clock_note(60.0, GroupId::Named(0))),
(2, clock_note(70.0, GroupId::Main)),
]
);
assert_runtime_in_both_copies(&storage, &[(1, 1), (2, 1), (3, 3)]);
}
#[test]
fn extraction_deduplicates_ids_and_removes_empty_group() {
let (mut world, _) = reversed_scroll_storage();
let removing = clock_note(30.0, GroupId::Named(0));
let mut storage = world.build_storage(vec![removing.clone(), clock_note(0.0, GroupId::Main)]);
let mut extracted = vec![];
storage.extract_notes([1, 1, 0].into_iter(), |id, note| extracted.push((id, note)));
assert_eq!(extracted, vec![(1, removing)]);
{
let groups = storage.time_sorted_groups();
assert_eq!(groups.len(), 1);
assert_eq!(groups[0].group_id(), GroupId::Main);
}
{
let groups = storage.scroll_sorted_groups();
assert_eq!(groups.len(), 1);
assert_eq!(groups[0].group_id(), GroupId::Main);
}
assert_runtime_in_both_copies(&storage, &[(2, 1)]);
}
#[test]
fn range_mutation_syncs_copies_and_includes_both_endpoints() {
let (_, mut storage) = reversed_scroll_storage();
let mut visits = vec![];
storage.notes_mut_overlapping_range(
Interval::new(
ClockTime::from_milis(30.0).unwrap(),
ClockTime::from_milis(30.0).unwrap(),
),
|state| {
visits.push(state.id);
*state.runtime_data.is_event_track_applied += 10;
},
);
visits.sort_unstable();
assert_eq!(visits, vec![1, 3]);
assert_runtime_in_both_copies(&storage, &[(1, 11), (2, 1), (3, 11)]);
}
#[test]
fn removing_all_notes_clears_both_copies_with_duplicate_and_unknown_ids() {
let (_, mut storage) = reversed_scroll_storage();
storage.remove_notes([1, 2, 2, 3, 0].into_iter());
assert!(storage.time_sorted_groups().is_empty());
assert!(storage.scroll_sorted_groups().is_empty());
assert!(storage.note_id().next().is_none());
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_interval(time: &Interval<Time>) -> Interval<ClockTime> {
let convert = |t: &Time| match t {
Time::Clock(clock_time) => *clock_time,
Time::Beat(_) => panic!("test notes should be in clock time"),
};
Interval::new(convert(&time.start()), convert(&time.end()))
}
proptest! {
#[test]
fn arb_query_notes_state_returns_originals(
(mut world, notes) in arb_starting_data(10, 100),
) {
let storage = world.build_storage(notes.clone());
let expected = expected_notes(¬es);
let all_ids = storage.note_id().collect::<Vec<_>>();
let states: Vec<_> =
storage.query_notes_ref(all_ids.iter().copied()).collect();
assert_eq!(states.len(), all_ids.len());
for state in states {
let note = expected.get(&state.id).expect("id should be valid");
let clock_time = clock_interval(¬e.time);
assert_eq!(state.start_time, note.start_time());
assert_eq!(state.duration, note.duration());
assert_eq!(state.clock_time.start(), clock_time.start());
assert_eq!(state.clock_time.end(), clock_time.end());
assert_eq!(state.group_id, note.group_id());
}
assert_eq!(storage.query_notes_ref(std::iter::once(0u64)).count(), 0);
storage.health_check();
}
}
proptest! {
#[test]
fn arb_notes_overlapping_range_matches_naive(
(mut world, notes) in arb_starting_data(10, 100),
(t1, t2) in (arb_clock_time(), arb_clock_time()),
) {
let storage = world.build_storage(notes.clone());
let expected = expected_notes(¬es);
let range = Interval::new(t1, t2);
let mut overlapping: Vec<u64> = storage
.notes_overlapping_range(range)
.map(|(id, _)| id)
.collect();
overlapping.sort();
let mut naive: Vec<u64> = expected
.iter()
.filter(|(_, note)| clock_interval(¬e.time).intersect(&range))
.map(|(id, _)| *id)
.collect();
naive.sort();
assert_eq!(overlapping, naive);
for (id, note) in storage.notes_overlapping_range(range) {
assert_eq!(expected.get(&id), Some(¬e));
}
storage.health_check();
}
}
proptest! {
#[test]
fn arb_query_notes_state_mut_mutates_data(
(mut world, notes, indices) in arb_starting_data_select_multi(10, 100, 100),
) {
let mut storage = world.build_storage(notes.clone());
let all_ids = storage.note_id().collect::<Vec<_>>();
let mut_ids: Vec<u64> = indices.iter().map(|&i| all_ids[i]).collect();
let mut visited = 0;
storage.query_notes_mut(
mut_ids.iter().copied().chain(mut_ids.iter().copied()),
|state| {
visited += 1;
*state.runtime_data.is_event_track_applied = 1;
},
);
assert_eq!(visited, mut_ids.len() * 2);
for &id in &mut_ids {
let mut states = storage.query_notes_ref(std::iter::once(id));
let state = states.next().expect("id should be valid");
assert_eq!(*state.runtime_data.is_event_track_applied, 1);
assert!(*state.value_data.is_event_track_applied);
}
storage.health_check();
}
}
proptest! {
#[test]
fn group_interval_queries_match_naive(
(mut world, notes) in arb_starting_data(10, 100),
(a, b) in (arb_clock_time(), arb_clock_time()),
) {
let storage = world.build_storage(notes);
let time_range = Interval::new(a, b);
for group in storage.time_sorted_groups() {
let mut actual: Vec<_> = group.query_time_intersecting(time_range).collect();
actual.sort_unstable();
let expected: Vec<_> = group.clock_time().iter().enumerate()
.filter(|(_, interval)| interval.intersect(&time_range)).map(|(i,_)| i).collect();
prop_assert_eq!(&actual, &expected);
let mut direct: Vec<_> = group.time_index().query_intersecting(time_range).collect();
direct.sort_unstable();
prop_assert_eq!(direct, expected);
}
for group in storage.scroll_sorted_groups() {
let note_group = world.note_groups.get_group(group.group_id()).unwrap();
let track = world.scroll_tracks.get_track(note_group.scroll_track_id()).unwrap();
let scroll_range = Interval::new(track.calculate_scroll_position(a), track.calculate_scroll_position(b));
let mut actual: Vec<_> = group.query_scroll_intersecting(scroll_range).collect();
actual.sort_unstable();
let expected: Vec<_> = group.scroll_position().iter().enumerate()
.filter(|(_, interval)| interval.intersect(&scroll_range)).map(|(i,_)| i).collect();
prop_assert_eq!(&actual, &expected);
let mut direct: Vec<_> = group.scroll_index().query_intersecting(scroll_range).collect();
direct.sort_unstable();
prop_assert_eq!(direct, expected);
}
}
}
#[test]
fn negative_scroll_query_includes_touching_intervals() {
let (world, storage) = reversed_scroll_storage();
let position = world
.scroll_tracks
.main_track()
.calculate_scroll_position(ClockTime::from_milis(30.0).unwrap());
for group in storage.scroll_sorted_groups() {
let mut ids: Vec<_> = group
.query_scroll_intersecting(Interval::new(position, position))
.map(|i| group.note_id()[i])
.collect();
ids.sort_unstable();
assert_eq!(ids, vec![1, 3]);
}
}
#[test]
fn components_follow_each_axis_veb_layout() {
let (mut world, _) = reversed_scroll_storage();
let notes: Vec<_> = (0..31)
.rev()
.map(|i| clock_note(i as f64 * 20.0, GroupId::Main))
.collect();
let mut storage = world.build_storage(notes.clone());
storage.query_notes_mut((1..=31).into_iter(), |state| {
*state.runtime_data.is_event_track_applied = state.id as u32;
});
let time = &storage.time_sorted_groups()[0];
let scroll = &storage.scroll_sorted_groups()[0];
let expected_time = IntervalTree::with_intervals(
notes
.iter()
.map(|note| clock_interval(¬e.time))
.collect(),
);
let track = world.scroll_tracks.main_track();
let expected_scroll = IntervalTree::with_intervals(
notes
.iter()
.map(|note| {
let time = clock_interval(¬e.time);
Interval::new(
track.calculate_scroll_position(time.start()),
track.calculate_scroll_position(time.end()),
)
})
.collect(),
);
assert_eq!(time.clock_time(), expected_time.intervals);
assert_eq!(scroll.scroll_position(), expected_scroll.intervals);
assert!(
!time
.clock_time()
.is_sorted_by_key(|interval| interval.start())
);
assert!(
!scroll
.scroll_position()
.is_sorted_by_key(|interval| interval.start())
);
for (i, &id) in time.note_id().iter().enumerate() {
assert_eq!(time.start_time()[i], notes[id as usize - 1].start_time());
assert_eq!(time.duration()[i], notes[id as usize - 1].duration());
}
for (i, &id) in scroll.note_id().iter().enumerate() {
assert_eq!(scroll.start_time()[i], notes[id as usize - 1].start_time());
assert_eq!(scroll.duration()[i], notes[id as usize - 1].duration());
}
assert_runtime_in_both_copies(
&storage,
&(1..=31).map(|id| (id, id as u32)).collect::<Vec<_>>(),
);
}
proptest! {
#[test]
fn rebuild_restores_indexes((mut world, notes) in arb_world(4, 0, 60)) {
let mut storage = world.build_storage(notes);
let mut ids: Vec<_> = storage.note_id().collect();
ids.sort_unstable();
let expected: Vec<_> = storage.query_notes(ids.iter().copied()).collect();
assert_event_track_calls(&storage, &ids.iter().map(|&id| (id, 1)).collect::<Vec<_>>());
storage.note_id_lookup.clear();
for group in &mut storage.time_sorted_groups {
group.time_index = IntervalTree::new();
}
for group in &mut storage.scroll_sorted_groups {
group.scroll_index = IntervalTree::new();
}
storage.rebuild_clock_time(
&world.note_groups,
&world.tempo_tracks,
&world.scroll_tracks,
);
storage.health_check();
let actual: Vec<_> = storage.query_notes(ids.iter().copied()).collect();
prop_assert_eq!(&actual, &expected);
assert_event_track_calls(&storage, &ids.iter().map(|&id| (id, 2)).collect::<Vec<_>>());
for group in &mut storage.time_sorted_groups {
group.time_index = IntervalTree::new();
}
for group in &mut storage.scroll_sorted_groups {
group.scroll_index = IntervalTree::new();
}
storage.rebuild_scroll_pos(
&world.note_groups,
&world.tempo_tracks,
&world.scroll_tracks,
);
storage.health_check();
assert_event_track_calls(&storage, &ids.iter().map(|&id| (id, 3)).collect::<Vec<_>>());
let actual: Vec<_> = storage.query_notes(ids.into_iter()).collect();
prop_assert_eq!(&actual, &expected);
}
}
#[test]
fn rebuild_after_changing_tempo_updates_clock_time_and_scroll_pos() {
let mut world = World::new(0);
world
.tempo_tracks
.insert_or_replace_track(TrackId::Main, test_tempo_track(120.0));
let mut storage = world.build_storage(vec![
TestNote {
time: Interval::try_new(
Time::Beat(BeatTime::new(1.0).unwrap()),
Time::Beat(BeatTime::new(2.0).unwrap()),
)
.unwrap(),
group_id: GroupId::Main,
},
TestNote {
time: Interval::try_new(
Time::Beat(BeatTime::new(2.0).unwrap()),
Time::Beat(BeatTime::new(3.0).unwrap()),
)
.unwrap(),
group_id: GroupId::Main,
},
]);
let initial: Vec<_> = storage
.query_notes_ref([1, 2].into_iter())
.map(|state| state.clock_time)
.collect();
assert_eq!(
initial,
vec![
Interval::new(
ClockTime::from_milis(500.0).unwrap(),
ClockTime::from_milis(1000.0).unwrap(),
),
Interval::new(
ClockTime::from_milis(1000.0).unwrap(),
ClockTime::from_milis(1500.0).unwrap(),
),
]
);
world
.tempo_tracks
.insert_or_replace_track(TrackId::Main, test_tempo_track(240.0));
storage.rebuild_clock_time(
&world.note_groups,
&world.tempo_tracks,
&world.scroll_tracks,
);
storage.health_check();
assert_event_track_calls(&storage, &[(1, 2), (2, 2)]);
let actual: Vec<_> = storage
.query_notes_ref([1, 2].into_iter())
.map(|state| state.clock_time)
.collect();
assert_eq!(
actual,
vec![
Interval::new(
ClockTime::from_milis(250.0).unwrap(),
ClockTime::from_milis(500.0).unwrap(),
),
Interval::new(
ClockTime::from_milis(500.0).unwrap(),
ClockTime::from_milis(750.0).unwrap(),
),
]
);
let group = storage
.time_sorted_groups()
.iter()
.find(|group| group.group_id() == GroupId::Main)
.unwrap();
assert_eq!(
group.scroll_position(),
&[
Interval::new(
ScrollPosition::new(0.25).unwrap(),
ScrollPosition::new(0.5).unwrap(),
),
Interval::new(
ScrollPosition::new(0.5).unwrap(),
ScrollPosition::new(0.75).unwrap(),
),
]
);
}
#[test]
fn rebuild_after_changing_scroll_speed_updates_scroll_pos() {
let mut world = World::new(0);
let mut storage = world.build_storage(vec![
clock_note(300.0, GroupId::Main),
clock_note(600.0, GroupId::Main),
]);
let group = storage
.time_sorted_groups()
.iter()
.find(|group| group.group_id() == GroupId::Main)
.unwrap();
assert_eq!(
group.scroll_position(),
&[
Interval::new(
ScrollPosition::new(0.3).unwrap(),
ScrollPosition::new(0.31).unwrap(),
),
Interval::new(
ScrollPosition::new(0.6).unwrap(),
ScrollPosition::new(0.61).unwrap(),
),
]
);
world.scroll_tracks.insert_or_replace_track(
TrackId::Main,
ScrollSpeedTrack::with_events(
TrackId::Main,
vec![
ScrollSpeedChangeEvent::new(Time::Clock(ClockTime::ZERO), 2.0, Easing::InConst)
.unwrap(),
],
&world.tempo_tracks,
),
);
storage.rebuild_scroll_pos(
&world.note_groups,
&world.tempo_tracks,
&world.scroll_tracks,
);
storage.health_check();
assert_event_track_calls(&storage, &[(1, 2), (2, 2)]);
let actual_clock_times: Vec<_> = storage
.query_notes_ref([1, 2].into_iter())
.map(|state| state.clock_time)
.collect();
assert_eq!(
actual_clock_times,
vec![
Interval::new(
ClockTime::from_milis(300.0).unwrap(),
ClockTime::from_milis(310.0).unwrap(),
),
Interval::new(
ClockTime::from_milis(600.0).unwrap(),
ClockTime::from_milis(610.0).unwrap(),
),
]
);
let group = storage
.time_sorted_groups()
.iter()
.find(|group| group.group_id() == GroupId::Main)
.unwrap();
assert_eq!(
group.scroll_position(),
&[
Interval::new(
ScrollPosition::new(0.6).unwrap(),
ScrollPosition::new(0.62).unwrap(),
),
Interval::new(
ScrollPosition::new(1.2).unwrap(),
ScrollPosition::new(1.22).unwrap(),
),
]
);
}