Skip to content

Commit 16891ff

Browse files
authored
feat(sdk): Mutation-based testing and property-based testing for LinkedChunk
feat(sdk): Mutation-based testing and property-based testing for `LinkedChunk`
2 parents 70403b5 + 78433d3 commit 16891ff

File tree

5 files changed

+191
-2
lines changed

5 files changed

+191
-2
lines changed

Cargo.lock

Lines changed: 48 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/matrix-sdk/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ tracing-subscriber = { version = "0.3.11", features = ["env-filter"] }
151151
wasm-bindgen-test = "0.3.33"
152152

153153
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
154+
proptest = "1.4.0"
154155
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
155156
wiremock = { workspace = true }
156157

crates/matrix-sdk/src/event_cache/linked_chunk/as_vector.rs

Lines changed: 110 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ type ChunkLength = usize;
3333
/// `Vec<VectorDiff<Item>>` (this type). Basically, it helps to consume a
3434
/// [`LinkedChunk<CAP, Item, Gap>`](super::LinkedChunk) as if it was an
3535
/// [`eyeball_im::ObservableVector<Item>`].
36+
#[derive(Debug)]
3637
pub struct AsVector<Item, Gap> {
3738
/// Strong reference to [`UpdatesInner`].
3839
updates: Arc<RwLock<UpdatesInner<Item, Gap>>>,
@@ -58,14 +59,20 @@ impl<Item, Gap> AsVector<Item, Gap> {
5859
token: ReaderToken,
5960
chunk_iterator: Iter<'_, CAP, Item, Gap>,
6061
) -> Self {
62+
// Drain previous updates so that this type is synced with `Updates`.
63+
{
64+
let mut updates = updates.write().unwrap();
65+
let _ = updates.take_with_token(token);
66+
}
67+
6168
Self { updates, token, mapper: UpdateToVectorDiff::new(chunk_iterator) }
6269
}
6370

6471
/// Take the new updates as [`VectorDiff`].
6572
///
6673
/// It returns an empty `Vec` if there is no new `VectorDiff` for the
6774
/// moment.
68-
pub(super) fn take(&mut self) -> Vec<VectorDiff<Item>>
75+
pub fn take(&mut self) -> Vec<VectorDiff<Item>>
6976
where
7077
Item: Clone,
7178
{
@@ -76,6 +83,7 @@ impl<Item, Gap> AsVector<Item, Gap> {
7683
}
7784

7885
/// Internal type that converts [`Update`] into [`VectorDiff`].
86+
#[derive(Debug)]
7987
struct UpdateToVectorDiff {
8088
/// Pairs of all known chunks and their respective length. This is the only
8189
/// required data for this algorithm.
@@ -581,4 +589,105 @@ mod tests {
581589
]
582590
);
583591
}
592+
593+
#[test]
594+
fn updates_are_drained_when_constructing_as_vector() {
595+
let mut linked_chunk = LinkedChunk::<10, char, ()>::new_with_update_history();
596+
597+
linked_chunk.push_items_back(['a']);
598+
599+
let mut as_vector = linked_chunk.as_vector().unwrap();
600+
let diffs = as_vector.take();
601+
602+
// `diffs` are empty because `AsVector` is built _after_ `LinkedChunk`
603+
// has been updated.
604+
assert!(diffs.is_empty());
605+
606+
linked_chunk.push_items_back(['b']);
607+
608+
let diffs = as_vector.take();
609+
610+
// `diffs` is not empty because new updates are coming.
611+
assert_eq!(diffs.len(), 1);
612+
}
613+
614+
#[cfg(not(target_arch = "wasm32"))]
615+
mod proptests {
616+
use proptest::prelude::*;
617+
618+
use super::*;
619+
620+
#[derive(Debug, Clone)]
621+
enum AsVectorOperation {
622+
PushItems { items: Vec<char> },
623+
PushGap,
624+
ReplaceLastGap { items: Vec<char> },
625+
}
626+
627+
fn as_vector_operation_strategy() -> impl Strategy<Value = AsVectorOperation> {
628+
prop_oneof![
629+
3 => prop::collection::vec(prop::char::ranges(vec!['a'..='z', 'A'..='Z'].into()), 0..=25)
630+
.prop_map(|items| AsVectorOperation::PushItems { items }),
631+
632+
2 => Just(AsVectorOperation::PushGap),
633+
634+
1 => prop::collection::vec(prop::char::ranges(vec!['a'..='z', 'A'..='Z'].into()), 0..=25)
635+
.prop_map(|items| AsVectorOperation::ReplaceLastGap { items }),
636+
]
637+
}
638+
639+
proptest! {
640+
#[test]
641+
fn as_vector_is_correct(
642+
operations in prop::collection::vec(as_vector_operation_strategy(), 10..=50)
643+
) {
644+
let mut linked_chunk = LinkedChunk::<10, char, ()>::new_with_update_history();
645+
let mut as_vector = linked_chunk.as_vector().unwrap();
646+
647+
for operation in operations {
648+
match operation {
649+
AsVectorOperation::PushItems { items } => {
650+
linked_chunk.push_items_back(items);
651+
}
652+
653+
AsVectorOperation::PushGap => {
654+
linked_chunk.push_gap_back(());
655+
}
656+
657+
AsVectorOperation::ReplaceLastGap { items } => {
658+
let Some(gap_identifier) = linked_chunk
659+
.rchunks()
660+
.find_map(|chunk| chunk.is_gap().then_some(chunk.identifier()))
661+
else {
662+
continue;
663+
};
664+
665+
linked_chunk.replace_gap_at(items, gap_identifier).unwrap();
666+
}
667+
}
668+
}
669+
670+
let mut vector_from_diffs = Vec::new();
671+
672+
// Read all updates as `VectorDiff` and rebuild a `Vec<char>`.
673+
for diff in as_vector.take() {
674+
match diff {
675+
VectorDiff::Insert { index, value } => vector_from_diffs.insert(index, value),
676+
VectorDiff::Append { values } => {
677+
let mut values = values.iter().copied().collect();
678+
679+
vector_from_diffs.append(&mut values);
680+
}
681+
_ => unreachable!(),
682+
}
683+
}
684+
685+
// Iterate over all chunks and collect items as `Vec<char>`.
686+
let vector_from_chunks = linked_chunk.items().map(|(_, item)| *item).collect::<Vec<_>>();
687+
688+
// Compare both `Vec`s.
689+
assert_eq!(vector_from_diffs, vector_from_chunks);
690+
}
691+
}
692+
}
584693
}

crates/matrix-sdk/src/event_cache/linked_chunk/mod.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,12 @@ pub struct LinkedChunk<const CHUNK_CAPACITY: usize, Item, Gap> {
216216
marker: PhantomData<Box<Chunk<CHUNK_CAPACITY, Item, Gap>>>,
217217
}
218218

219+
impl<const CAP: usize, Item, Gap> Default for LinkedChunk<CAP, Item, Gap> {
220+
fn default() -> Self {
221+
Self::new()
222+
}
223+
}
224+
219225
impl<const CAP: usize, Item, Gap> LinkedChunk<CAP, Item, Gap> {
220226
/// Create a new [`Self`].
221227
pub fn new() -> Self {
@@ -252,6 +258,7 @@ impl<const CAP: usize, Item, Gap> LinkedChunk<CAP, Item, Gap> {
252258
}
253259

254260
/// Get the number of items in this linked chunk.
261+
#[allow(clippy::len_without_is_empty)]
255262
pub fn len(&self) -> usize {
256263
self.length
257264
}
@@ -865,6 +872,7 @@ impl Position {
865872

866873
/// An iterator over a [`LinkedChunk`] that traverses the chunk in backward
867874
/// direction (i.e. it calls `previous` on each chunk to make progress).
875+
#[derive(Debug)]
868876
pub struct IterBackward<'a, const CAP: usize, Item, Gap> {
869877
chunk: Option<&'a Chunk<CAP, Item, Gap>>,
870878
}
@@ -890,6 +898,7 @@ impl<'a, const CAP: usize, Item, Gap> Iterator for IterBackward<'a, CAP, Item, G
890898

891899
/// An iterator over a [`LinkedChunk`] that traverses the chunk in forward
892900
/// direction (i.e. it calls `next` on each chunk to make progress).
901+
#[derive(Debug)]
893902
pub struct Iter<'a, const CAP: usize, Item, Gap> {
894903
chunk: Option<&'a Chunk<CAP, Item, Gap>>,
895904
}
@@ -1248,6 +1257,8 @@ where
12481257

12491258
#[cfg(test)]
12501259
mod tests {
1260+
use std::ops::Not;
1261+
12511262
use assert_matches::assert_matches;
12521263

12531264
use super::{
@@ -2151,4 +2162,23 @@ mod tests {
21512162
assert_eq!(chunk.last_position(), Position(ChunkIdentifier(3), 0));
21522163
}
21532164
}
2165+
2166+
#[test]
2167+
fn test_is_first_and_last_chunk() {
2168+
let mut linked_chunk = LinkedChunk::<3, char, ()>::new();
2169+
2170+
let mut chunks = linked_chunk.chunks().peekable();
2171+
assert!(chunks.peek().unwrap().is_first_chunk());
2172+
assert!(chunks.next().unwrap().is_last_chunk());
2173+
assert!(chunks.next().is_none());
2174+
2175+
linked_chunk.push_items_back(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']);
2176+
2177+
let mut chunks = linked_chunk.chunks().peekable();
2178+
assert!(chunks.next().unwrap().is_first_chunk());
2179+
assert!(chunks.peek().unwrap().is_first_chunk().not());
2180+
assert!(chunks.next().unwrap().is_last_chunk().not());
2181+
assert!(chunks.next().unwrap().is_last_chunk());
2182+
assert!(chunks.next().is_none());
2183+
}
21542184
}

crates/matrix-sdk/src/event_cache/linked_chunk/updates.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ pub enum Update<Item, Gap> {
9595
/// A collection of [`Update`]s that can be observed.
9696
///
9797
/// Get a value for this type with [`LinkedChunk::updates`].
98+
#[derive(Debug)]
9899
pub struct ObservableUpdates<Item, Gap> {
99100
pub(super) inner: Arc<RwLock<UpdatesInner<Item, Gap>>>,
100101
}
@@ -164,6 +165,7 @@ pub(super) type ReaderToken = usize;
164165
/// for example with [`UpdatesSubscriber`]. Of course, they can be multiple
165166
/// `UpdatesSubscriber`s at the same time. Hence the need of supporting multiple
166167
/// readers.
168+
#[derive(Debug)]
167169
pub(super) struct UpdatesInner<Item, Gap> {
168170
/// All the updates that have not been read by all readers.
169171
updates: Vec<Update<Item, Gap>>,

0 commit comments

Comments
 (0)