use proptest::prelude::*;
use crate::math::Easing;
use crate::rhythm::{
ScrollSpeedChangeEvent, ScrollSpeedTrack, Tempo, TempoChangeEvent, TempoTrack,
};
use super::*;
use crate::notes::{Interval, NoteGroup, NoteGroups};
use crate::rhythm::{ScrollSpeedTracks, TempoTracks};
#[derive(Debug, Clone, PartialEq)]
pub struct TestNote {
time: Time,
group_id: GroupId,
}
#[derive(Debug, Soars, SoaClone, Clone, Copy)]
pub struct TestNoteValue {
unused: u32,
}
#[derive(Debug, Soars, SoaClone, Clone, Copy, Default)]
pub struct TestNoteRuntime {
unused: u32,
}
impl ShortNoteData for TestNote {
type ValueData = TestNoteValue;
type RuntimeData = TestNoteRuntime;
fn time(&self) -> Time {
self.time
}
fn group_id(&self) -> GroupId {
self.group_id
}
fn value_data(&self) -> Self::ValueData {
TestNoteValue { unused: 0 }
}
fn reconstruct<'a>(
time: Time,
_value_data: <Self::ValueData as soa_rs::Soars>::Ref<'a>,
group_id: GroupId,
) -> Self {
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>) -> ShortNoteTypeStorage<TestNote> {
ShortNoteTypeStorage::with_notes(
&mut self.note_groups,
&mut self.tempo_tracks,
&mut self.scroll_tracks,
notes.into_iter(),
)
}
fn add_note(&mut self, storage: &mut ShortNoteTypeStorage<TestNote>, note: TestNote) {
storage.add_note(
&mut self.note_groups,
&mut self.tempo_tracks,
&mut self.scroll_tracks,
note,
);
}
fn add_notes(
&mut self,
storage: &mut ShortNoteTypeStorage<TestNote>,
notes: impl Iterator<Item = TestNote>,
) {
storage.add_notes(
&mut self.note_groups,
&mut self.tempo_tracks,
&mut self.scroll_tracks,
notes,
);
}
fn replace_note(
&mut self,
storage: &mut ShortNoteTypeStorage<TestNote>,
id: u64,
note: TestNote,
) {
storage.replace_note(
&mut self.note_groups,
&mut self.tempo_tracks,
&mut self.scroll_tracks,
id,
note,
);
}
fn replace_notes(
&mut self,
storage: &mut ShortNoteTypeStorage<TestNote>,
replaces: impl Iterator<Item = (u64, TestNote)>,
) {
storage.replace_notes(
&mut self.note_groups,
&mut self.tempo_tracks,
&mut 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 clock_note(ms: f64, group_id: GroupId) -> TestNote {
TestNote {
time: Time::Clock(ClockTime::from_milis(ms).unwrap()),
group_id,
}
}
fn arb_time() -> impl Strategy<Value = Time> {
(-600000..600000i32).prop_map(|t| Time::Clock(ClockTime::from_milis(t as f64).unwrap()))
}
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),
)
})
}
#[test]
fn add_note_to_empty_stays_consistent() {
let mut world = World::new(0);
let mut storage = ShortNoteTypeStorage::new();
world.add_note(
&mut storage,
TestNote {
time: Time::Clock(ClockTime::from_milis(1.0).unwrap()),
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: Time::Clock(ClockTime::from_milis(1.0).unwrap()),
group_id: GroupId::Main,
}]);
world.add_note(
&mut storage,
TestNote {
time: Time::Clock(ClockTime::ZERO),
group_id: GroupId::Main,
},
);
storage.health_check();
}
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);
storage.health_check();
}
}
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.time_sorted_groups().is_empty())
}
}
fn reversed_scroll_storage() -> (World, ShortNoteTypeStorage<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(10.0, GroupId::Main),
clock_note(20.0, GroupId::Main),
]);
(world, storage)
}
fn assert_runtime_in_both_copies(
storage: &ShortNoteTypeStorage<TestNote>,
expected: &[(u64, u32)],
) {
storage.health_check();
for groups in [storage.time_sorted_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().unused))
})
.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.unused += state.id as u32;
});
assert_eq!(visits, vec![1, 2, 3, 2]);
assert_runtime_in_both_copies(&storage, &[(1, 1), (2, 4), (3, 3)]);
}
#[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.unused = 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, 2), (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(10.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)]);
for groups in [storage.time_sorted_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, 0)]);
}
#[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(20.0).unwrap(),
ClockTime::from_milis(30.0).unwrap(),
),
|state| {
visits.push(state.id);
*state.runtime_data.unused += 10;
},
);
assert_eq!(visits, vec![3, 1]);
assert_runtime_in_both_copies(&storage, &[(1, 10), (2, 0), (3, 10)]);
}
#[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())
}
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)| match note.time {
Time::Clock(clock_time) => range.contains(&clock_time),
Time::Beat(_) => panic!("test notes should be in clock time"),
})
.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.unused = 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.unused, 1);
assert_eq!(*state.value_data.unused, 0);
}
storage.health_check();
}
}
#[test]
fn query_empty_tree() {
let data = Vec::<u32>::new();
let result = query_intersecting(&data, Interval::new(0, 100)).collect::<Vec<_>>();
assert!(result.is_empty())
}
fn arb_interval() -> impl Strategy<Value = Interval<u32>> {
(any::<u32>(), any::<u32>()).prop_map(|(v1, v2)| Interval::new(v1, v2))
}
fn arb_data(max_len: usize) -> impl Strategy<Value = Vec<u32>> {
prop::collection::vec(any::<u32>(), 0..max_len)
}
fn test_query(query: Interval<u32>, mut data: Vec<u32>) {
data.sort();
let binary_result = query_intersecting(&data, query).collect::<Vec<_>>();
let mut binary_result = binary_result
.into_iter()
.map(|i| data[i])
.collect::<Vec<_>>();
let mut naive_result = data
.into_iter()
.filter(|i| query.contains(i))
.collect::<Vec<_>>();
binary_result.sort();
naive_result.sort();
assert_eq!(binary_result, naive_result);
}
proptest! {
#[test]
fn query_arb_intervals(query in arb_interval(), intervals in arb_data(100)) {
test_query(query, intervals);
}
}
proptest! {
#[ignore]
#[test]
fn query_arb_intervals_large(query in arb_interval(), intervals in arb_data(10000)) {
test_query(query, intervals);
}
}
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();
}
}
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 = match note.time {
Time::Clock(clock_time) => clock_time,
Time::Beat(_) => panic!("test notes should be in clock time"),
};
assert_eq!(state.start_time, note.time);
assert_eq!(state.duration, Time::Clock(ClockTime::ZERO));
assert_eq!(state.clock_time.start(), clock_time);
assert_eq!(state.clock_time.end(), clock_time);
assert_eq!(state.group_id, ShortNoteData::group_id(note));
}
assert_eq!(storage.query_notes_ref(std::iter::once(0u64)).count(), 0);
storage.health_check();
}
}
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();
storage.note_id_lookup.clear();
for group in storage.time_sorted_groups.iter_mut().chain(storage.scroll_sorted_groups.iter_mut()) {
if group.note_id.len() > 1 {
let last = group.note_id.len() - 1;
group.note_id.swap(0, last);
group.time.swap(0, last);
group.clock_time.swap(0, last);
group.scroll_position.swap(0, last);
group.value_data.swap(0, last);
group.runtime_data.swap(0, last);
}
}
storage.rebuild_clock_time(
&mut world.note_groups,
&mut world.tempo_tracks,
&mut world.scroll_tracks,
);
storage.health_check();
let actual: Vec<_> = storage.query_notes(ids.iter().copied()).collect();
prop_assert_eq!(&actual, &expected);
storage.rebuild_scroll_pos(
&mut world.note_groups,
&mut world.tempo_tracks,
&mut world.scroll_tracks,
);
storage.health_check();
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(100.0));
let mut storage = world.build_storage(vec![
TestNote {
time: Time::Beat(BeatTime::new(1.0).unwrap()),
group_id: GroupId::Main,
},
TestNote {
time: Time::Beat(BeatTime::new(2.0).unwrap()),
group_id: GroupId::Main,
},
]);
let initial: Vec<_> = storage
.query_notes_ref([1, 2].into_iter())
.map(|state| state.clock_time.start())
.collect();
assert_eq!(
initial,
vec![
ClockTime::from_milis(600.0).unwrap(),
ClockTime::from_milis(1200.0).unwrap(),
]
);
world
.tempo_tracks
.insert_or_replace_track(TrackId::Main, test_tempo_track(200.0));
storage.rebuild_clock_time(
&mut world.note_groups,
&mut world.tempo_tracks,
&mut world.scroll_tracks,
);
storage.health_check();
let actual: Vec<_> = storage
.query_notes_ref([1, 2].into_iter())
.map(|state| state.clock_time.start())
.collect();
assert_eq!(
actual,
vec![
ClockTime::from_milis(300.0).unwrap(),
ClockTime::from_milis(600.0).unwrap(),
]
);
let group = storage
.time_sorted_groups()
.iter()
.find(|group| group.group_id() == &GroupId::Main)
.unwrap();
assert_eq!(
group.scroll_position(),
&[
ScrollPosition::new(0.3).unwrap(),
ScrollPosition::new(0.6).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(),
&[
ScrollPosition::new(0.3).unwrap(),
ScrollPosition::new(0.6).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(
&mut world.note_groups,
&mut world.tempo_tracks,
&mut world.scroll_tracks,
);
storage.health_check();
let actual_clock_times: Vec<_> = storage
.query_notes_ref([1, 2].into_iter())
.map(|state| state.clock_time.start())
.collect();
assert_eq!(
actual_clock_times,
vec![
ClockTime::from_milis(300.0).unwrap(),
ClockTime::from_milis(600.0).unwrap(),
]
);
let group = storage
.time_sorted_groups()
.iter()
.find(|group| group.group_id() == &GroupId::Main)
.unwrap();
assert_eq!(
group.scroll_position(),
&[
ScrollPosition::new(0.6).unwrap(),
ScrollPosition::new(1.2).unwrap(),
]
);
}