matrix_sdk_common/linked_chunk/
lazy_loader.rs

1// Copyright 2024 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::marker::PhantomData;
16
17use tracing::error;
18
19use super::{
20    Chunk, ChunkContent, ChunkIdentifier, ChunkIdentifierGenerator, Ends, LinkedChunk,
21    ObservableUpdates, RawChunk, Update,
22};
23
24/// Build a new `LinkedChunk` with a single chunk that is supposed to be the
25/// last one.
26pub fn from_last_chunk<const CAP: usize, Item, Gap>(
27    chunk: Option<RawChunk<Item, Gap>>,
28    chunk_identifier_generator: ChunkIdentifierGenerator,
29) -> Result<Option<LinkedChunk<CAP, Item, Gap>>, LazyLoaderError> {
30    let Some(mut chunk) = chunk else {
31        return Ok(None);
32    };
33
34    // Check consistency before creating the `LinkedChunk`.
35    {
36        // The number of items is not too large.
37        if let ChunkContent::Items(items) = &chunk.content {
38            if items.len() > CAP {
39                return Err(LazyLoaderError::ChunkTooLarge { id: chunk.identifier });
40            }
41        }
42
43        // Chunk has no next chunk.
44        if chunk.next.is_some() {
45            return Err(LazyLoaderError::ChunkIsNotLast { id: chunk.identifier });
46        }
47    }
48
49    // Create the `LinkedChunk` from a single chunk.
50    {
51        // Take the `previous` chunk and consider it becomes the `lazy_previous`.
52        let lazy_previous = chunk.previous.take();
53
54        // Transform the `RawChunk` into a `Chunk`.
55        let mut chunk_ptr = Chunk::new_leaked(chunk.identifier, chunk.content);
56
57        // Set the `lazy_previous` value!
58        //
59        // SAFETY: Pointer is convertible to a reference.
60        unsafe { chunk_ptr.as_mut() }.lazy_previous = lazy_previous;
61
62        Ok(Some(LinkedChunk {
63            links: Ends { first: chunk_ptr, last: None },
64            chunk_identifier_generator,
65            updates: Some(ObservableUpdates::new()),
66            marker: PhantomData,
67        }))
68    }
69}
70
71/// Insert a new chunk at the front of a `LinkedChunk`.
72pub fn insert_new_first_chunk<const CAP: usize, Item, Gap>(
73    linked_chunk: &mut LinkedChunk<CAP, Item, Gap>,
74    mut new_first_chunk: RawChunk<Item, Gap>,
75) -> Result<(), LazyLoaderError>
76where
77    Item: Clone,
78    Gap: Clone,
79{
80    // Check `LinkedChunk` is going to be consistent after the insertion.
81    {
82        // The number of items is not too large.
83        if let ChunkContent::Items(items) = &new_first_chunk.content {
84            if items.len() > CAP {
85                return Err(LazyLoaderError::ChunkTooLarge { id: new_first_chunk.identifier });
86            }
87        }
88
89        // New chunk doesn't create a cycle.
90        if let Some(previous_chunk) = new_first_chunk.previous {
91            if linked_chunk.chunks().any(|chunk| chunk.identifier() == previous_chunk) {
92                return Err(LazyLoaderError::Cycle {
93                    new_chunk: new_first_chunk.identifier,
94                    with_chunk: previous_chunk,
95                });
96            }
97        }
98
99        let first_chunk = linked_chunk.links.first_chunk();
100        let expected_next_chunk = first_chunk.identifier();
101
102        // New chunk has a next chunk.
103        let Some(next_chunk) = new_first_chunk.next else {
104            return Err(LazyLoaderError::MissingNextChunk { id: new_first_chunk.identifier });
105        };
106
107        // New chunk has a next chunk, and it is the first chunk of the `LinkedChunk`.
108        if next_chunk != expected_next_chunk {
109            return Err(LazyLoaderError::CannotConnectTwoChunks {
110                new_chunk: new_first_chunk.identifier,
111                with_chunk: expected_next_chunk,
112            });
113        }
114
115        // Same check as before, but in reverse: the first chunk has a `lazy_previous`
116        // to the new first chunk.
117        if first_chunk.lazy_previous() != Some(new_first_chunk.identifier) {
118            return Err(LazyLoaderError::CannotConnectTwoChunks {
119                new_chunk: first_chunk.identifier,
120                with_chunk: new_first_chunk.identifier,
121            });
122        }
123
124        // Alright. All checks are made.
125    }
126
127    // Insert the new first chunk.
128    {
129        // Transform the `RawChunk` into a `Chunk`.
130        let lazy_previous = new_first_chunk.previous.take();
131        let mut new_first_chunk =
132            Chunk::new_leaked(new_first_chunk.identifier, new_first_chunk.content);
133
134        let links = &mut linked_chunk.links;
135
136        // Update the first chunk.
137        {
138            let first_chunk = links.first_chunk_mut();
139
140            debug_assert!(
141                first_chunk.previous.is_none(),
142                "The first chunk is not supposed to have a previous chunk"
143            );
144
145            // Move the `lazy_previous` if any.
146            first_chunk.lazy_previous = None;
147            unsafe { new_first_chunk.as_mut() }.lazy_previous = lazy_previous;
148
149            // Link one way: `new_first_chunk` becomes the previous chunk of the first
150            // chunk.
151            first_chunk.previous = Some(new_first_chunk);
152        }
153
154        // Update `links`.
155        {
156            // Remember the pointer to the `first_chunk`.
157            let old_first_chunk = links.first;
158
159            // `new_first_chunk` becomes the new first chunk.
160            links.first = new_first_chunk;
161
162            // Link the other way: `old_first_chunk` becomes the next chunk of the first
163            // chunk.
164            links.first_chunk_mut().next = Some(old_first_chunk);
165
166            debug_assert!(
167                links.first_chunk().previous.is_none(),
168                "The new first chunk is not supposed to have a previous chunk"
169            );
170
171            // Update the last chunk. If it's `Some(_)`, no need to update the last chunk
172            // pointer. If it's `None`, it means we had only one chunk; now we have two, the
173            // last chunk is the `old_first_chunk`.
174            if links.last.is_none() {
175                links.last = Some(old_first_chunk);
176            }
177        }
178    }
179
180    // Emit the updates.
181    if let Some(updates) = linked_chunk.updates.as_mut() {
182        let first_chunk = linked_chunk.links.first_chunk();
183        emit_new_first_chunk_updates(first_chunk, updates);
184    }
185
186    Ok(())
187}
188
189/// Emit updates whenever a new first chunk is inserted at the front of a
190/// `LinkedChunk`.
191fn emit_new_first_chunk_updates<const CAP: usize, Item, Gap>(
192    chunk: &Chunk<CAP, Item, Gap>,
193    updates: &mut ObservableUpdates<Item, Gap>,
194) where
195    Item: Clone,
196    Gap: Clone,
197{
198    let previous = chunk.previous().map(Chunk::identifier).or(chunk.lazy_previous);
199    let new = chunk.identifier();
200    let next = chunk.next().map(Chunk::identifier);
201
202    match chunk.content() {
203        ChunkContent::Gap(gap) => {
204            updates.push(Update::NewGapChunk { previous, new, next, gap: gap.clone() });
205        }
206        ChunkContent::Items(items) => {
207            updates.push(Update::NewItemsChunk { previous, new, next });
208            updates.push(Update::PushItems { at: chunk.first_position(), items: items.clone() });
209        }
210    }
211}
212
213/// Replace the items with the given last chunk of items and generator.
214///
215/// This clears all the chunks in memory before resetting to the new chunk,
216/// if provided.
217pub fn replace_with<const CAP: usize, Item, Gap>(
218    linked_chunk: &mut LinkedChunk<CAP, Item, Gap>,
219    chunk: Option<RawChunk<Item, Gap>>,
220    chunk_identifier_generator: ChunkIdentifierGenerator,
221) -> Result<(), LazyLoaderError>
222where
223    Item: Clone,
224    Gap: Clone,
225{
226    let Some(mut chunk) = chunk else {
227        // This is equivalent to clearing the linked chunk, and overriding the chunk ID
228        // generator afterwards. But, if there was no chunks in the DB, the generator
229        // should be reset too, so it's entirely equivalent to a clear.
230        linked_chunk.clear();
231        return Ok(());
232    };
233
234    // Check consistency before replacing the `LinkedChunk`.
235    // The number of items is not too large.
236    if let ChunkContent::Items(items) = &chunk.content {
237        if items.len() > CAP {
238            return Err(LazyLoaderError::ChunkTooLarge { id: chunk.identifier });
239        }
240    }
241
242    // Chunk has no next chunk.
243    if chunk.next.is_some() {
244        return Err(LazyLoaderError::ChunkIsNotLast { id: chunk.identifier });
245    }
246
247    // The last chunk is now valid.
248    linked_chunk.chunk_identifier_generator = chunk_identifier_generator;
249
250    // Take the `previous` chunk and consider it becomes the `lazy_previous`.
251    let lazy_previous = chunk.previous.take();
252
253    // Transform the `RawChunk` into a `Chunk`.
254    let mut chunk_ptr = Chunk::new_leaked(chunk.identifier, chunk.content);
255
256    // Set the `lazy_previous` value!
257    //
258    // SAFETY: Pointer is convertible to a reference.
259    unsafe { chunk_ptr.as_mut() }.lazy_previous = lazy_previous;
260
261    // Replace the first link with the new pointer.
262    linked_chunk.links.replace_with(chunk_ptr);
263
264    if let Some(updates) = linked_chunk.updates.as_mut() {
265        // Clear the previous updates, as we're about to insert a clear they would be
266        // useless.
267        updates.clear_pending();
268        updates.push(Update::Clear);
269
270        emit_new_first_chunk_updates(linked_chunk.links.first_chunk(), updates);
271    }
272
273    Ok(())
274}
275
276/// A pretty inefficient, test-only, function to rebuild a full `LinkedChunk`.
277#[doc(hidden)]
278pub fn from_all_chunks<const CAP: usize, Item, Gap>(
279    mut chunks: Vec<RawChunk<Item, Gap>>,
280) -> Result<Option<LinkedChunk<CAP, Item, Gap>>, LazyLoaderError>
281where
282    Item: Clone,
283    Gap: Clone,
284{
285    if chunks.is_empty() {
286        return Ok(None);
287    }
288
289    // Sort by `next` so that the search for the next chunk is faster (it should
290    // come first). The chunk with the biggest next chunk identifier comes first.
291    // Chunk with no next chunk comes last.
292    chunks.sort_by(|a, b| b.next.cmp(&a.next));
293
294    let last_chunk = chunks
295        .pop()
296        // SAFETY: `chunks` is guaranteed to not be empty, `pop` cannot fail.
297        .expect("`chunks` is supposed to not be empty, we must be able to `pop` an item");
298    let last_chunk_identifier = last_chunk.identifier;
299    let chunk_identifier_generator =
300        ChunkIdentifierGenerator::new_from_previous_chunk_identifier(last_chunk_identifier);
301
302    let Some(mut linked_chunk) = from_last_chunk(Some(last_chunk), chunk_identifier_generator)?
303    else {
304        return Ok(None);
305    };
306
307    let mut next_chunk = last_chunk_identifier;
308
309    while let Some(chunk) = chunks
310        .iter()
311        .position(|chunk| chunk.next == Some(next_chunk))
312        .map(|index| chunks.remove(index))
313    {
314        next_chunk = chunk.identifier;
315        insert_new_first_chunk(&mut linked_chunk, chunk)?;
316    }
317
318    let first_chunk = linked_chunk.links.first_chunk();
319
320    // It is expected that **all chunks** are passed to this function. If there was
321    // a previous chunk, `insert_new_first_chunk` has erased it and moved it to
322    // `lazy_previous`. Hence, let's check both (the former condition isn't
323    // necessary, but better be robust).
324    if first_chunk.previous().is_some() || first_chunk.lazy_previous.is_some() {
325        return Err(LazyLoaderError::ChunkIsNotFirst { id: first_chunk.identifier() });
326    }
327
328    if !chunks.is_empty() {
329        return Err(LazyLoaderError::MultipleConnectedComponents);
330    }
331
332    Ok(Some(linked_chunk))
333}
334
335#[derive(thiserror::Error, Debug)]
336pub enum LazyLoaderError {
337    #[error("chunk with id {} has a next chunk, it is supposed to be the last chunk", id.index())]
338    ChunkIsNotLast { id: ChunkIdentifier },
339
340    #[error("chunk with id {} forms a cycle with chunk with id {}", new_chunk.index(), with_chunk.index())]
341    Cycle { new_chunk: ChunkIdentifier, with_chunk: ChunkIdentifier },
342
343    #[error("chunk with id {} is supposed to have a next chunk", id.index())]
344    MissingNextChunk { id: ChunkIdentifier },
345
346    #[error(
347        "chunk with id {} cannot be connected to chunk with id {} because the identifiers do not match",
348        new_chunk.index(),
349        with_chunk.index()
350    )]
351    CannotConnectTwoChunks { new_chunk: ChunkIdentifier, with_chunk: ChunkIdentifier },
352
353    #[error("chunk with id {} is too large", id.index())]
354    ChunkTooLarge { id: ChunkIdentifier },
355
356    #[doc(hidden)]
357    #[error("the last chunk is missing")]
358    MissingLastChunk,
359
360    #[doc(hidden)]
361    #[error("chunk with id {} has a previous chunk, it is supposed to be the first chunk", id.index())]
362    ChunkIsNotFirst { id: ChunkIdentifier },
363
364    #[doc(hidden)]
365    #[error("multiple connected components")]
366    MultipleConnectedComponents,
367}
368
369#[cfg(test)]
370mod tests {
371    use assert_matches::assert_matches;
372
373    use super::{
374        super::Position, from_all_chunks, from_last_chunk, insert_new_first_chunk, replace_with,
375        ChunkContent, ChunkIdentifier, ChunkIdentifierGenerator, LazyLoaderError, LinkedChunk,
376        RawChunk, Update,
377    };
378
379    #[test]
380    fn test_from_last_chunk_err_too_much_items() {
381        let last_chunk = RawChunk {
382            previous: None,
383            identifier: ChunkIdentifier::new(0),
384            next: None,
385            content: ChunkContent::Items(vec!['a', 'b', 'c']),
386        };
387        let chunk_identifier_generator =
388            ChunkIdentifierGenerator::new_from_previous_chunk_identifier(ChunkIdentifier::new(0));
389
390        let maybe_linked_chunk =
391            from_last_chunk::<2, char, ()>(Some(last_chunk), chunk_identifier_generator);
392
393        assert_matches!(
394            maybe_linked_chunk,
395            Err(LazyLoaderError::ChunkTooLarge { id }) => {
396                assert_eq!(id, 0);
397            }
398        );
399    }
400
401    #[test]
402    fn test_from_last_chunk_err_is_not_last_chunk() {
403        let last_chunk = RawChunk {
404            previous: None,
405            identifier: ChunkIdentifier::new(0),
406            next: Some(ChunkIdentifier::new(42)),
407            content: ChunkContent::Items(vec!['a']),
408        };
409        let chunk_identifier_generator =
410            ChunkIdentifierGenerator::new_from_previous_chunk_identifier(ChunkIdentifier::new(0));
411
412        let maybe_linked_chunk =
413            from_last_chunk::<2, char, ()>(Some(last_chunk), chunk_identifier_generator);
414
415        assert_matches!(
416            maybe_linked_chunk,
417            Err(LazyLoaderError::ChunkIsNotLast { id }) => {
418                assert_eq!(id, 0);
419            }
420        );
421    }
422
423    #[test]
424    fn test_from_last_chunk_none() {
425        let chunk_identifier_generator =
426            ChunkIdentifierGenerator::new_from_previous_chunk_identifier(ChunkIdentifier::new(0));
427
428        let maybe_linked_chunk =
429            from_last_chunk::<2, char, ()>(None, chunk_identifier_generator).unwrap();
430
431        assert!(maybe_linked_chunk.is_none());
432    }
433
434    #[test]
435    fn test_from_last_chunk() {
436        let last_chunk = RawChunk {
437            previous: Some(ChunkIdentifier::new(42)),
438            identifier: ChunkIdentifier::new(0),
439            next: None,
440            content: ChunkContent::Items(vec!['a']),
441        };
442        let chunk_identifier_generator =
443            ChunkIdentifierGenerator::new_from_previous_chunk_identifier(ChunkIdentifier::new(0));
444
445        let maybe_linked_chunk =
446            from_last_chunk::<2, char, ()>(Some(last_chunk), chunk_identifier_generator).unwrap();
447
448        assert_matches!(maybe_linked_chunk, Some(mut linked_chunk) => {
449            let mut chunks = linked_chunk.chunks();
450
451            assert_matches!(chunks.next(), Some(chunk) => {
452                assert_eq!(chunk.identifier(), 0);
453                // The chunk's previous has been set to `None`
454                assert!(chunk.previous().is_none());
455            });
456            assert!(chunks.next().is_none());
457
458            // It has updates enabled.
459            assert!(linked_chunk.updates().is_some());
460        });
461    }
462
463    #[test]
464    fn test_insert_new_first_chunk_err_too_much_items() {
465        let new_first_chunk = RawChunk {
466            previous: None,
467            identifier: ChunkIdentifier::new(0),
468            next: None,
469            content: ChunkContent::Items(vec!['a', 'b', 'c']),
470        };
471
472        let mut linked_chunk = LinkedChunk::<2, char, ()>::new();
473
474        let result = insert_new_first_chunk(&mut linked_chunk, new_first_chunk);
475
476        assert_matches!(result, Err(LazyLoaderError::ChunkTooLarge { id }) => {
477            assert_eq!(id, 0);
478        });
479    }
480
481    #[test]
482    fn test_insert_new_first_chunk_err_cycle() {
483        let new_first_chunk = RawChunk {
484            previous: Some(ChunkIdentifier::new(0)),
485            identifier: ChunkIdentifier::new(1),
486            next: Some(ChunkIdentifier::new(0)),
487            content: ChunkContent::Gap(()),
488        };
489
490        let mut linked_chunk = LinkedChunk::<2, char, ()>::new();
491        let result = insert_new_first_chunk(&mut linked_chunk, new_first_chunk);
492
493        assert_matches!(result, Err(LazyLoaderError::Cycle { new_chunk, with_chunk }) => {
494            assert_eq!(new_chunk, 1);
495            assert_eq!(with_chunk, 0);
496        });
497    }
498
499    #[test]
500    fn test_insert_new_first_chunk_err_missing_next_chunk() {
501        let new_first_chunk = RawChunk {
502            previous: None,
503            identifier: ChunkIdentifier::new(0),
504            next: None,
505            content: ChunkContent::Gap(()),
506        };
507
508        let mut linked_chunk = LinkedChunk::<2, char, ()>::new();
509
510        let result = insert_new_first_chunk(&mut linked_chunk, new_first_chunk);
511
512        assert_matches!(result, Err(LazyLoaderError::MissingNextChunk { id }) => {
513            assert_eq!(id, 0);
514        });
515    }
516
517    #[test]
518    fn test_insert_new_first_chunk_err_cannot_connect_two_chunks() {
519        let new_first_chunk = RawChunk {
520            previous: None,
521            identifier: ChunkIdentifier::new(1),
522            next: Some(ChunkIdentifier::new(42)),
523            content: ChunkContent::Gap(()),
524        };
525
526        let mut linked_chunk = LinkedChunk::<2, char, ()>::new();
527        linked_chunk.push_gap_back(());
528
529        let result = insert_new_first_chunk(&mut linked_chunk, new_first_chunk);
530
531        assert_matches!(result, Err(LazyLoaderError::CannotConnectTwoChunks { new_chunk, with_chunk }) => {
532            assert_eq!(new_chunk, 1);
533            assert_eq!(with_chunk, 0);
534        });
535    }
536
537    #[test]
538    fn test_insert_new_first_chunk_err_cannot_connect_two_chunks_before_no_lazy_previous() {
539        let new_first_chunk = RawChunk {
540            previous: None,
541            identifier: ChunkIdentifier::new(1),
542            next: Some(ChunkIdentifier::new(0)),
543            content: ChunkContent::Gap(()),
544        };
545
546        let mut linked_chunk = LinkedChunk::<2, char, ()>::new();
547        linked_chunk.push_gap_back(());
548
549        let result = insert_new_first_chunk(&mut linked_chunk, new_first_chunk);
550
551        assert_matches!(result, Err(LazyLoaderError::CannotConnectTwoChunks { new_chunk, with_chunk }) => {
552            assert_eq!(new_chunk, 0);
553            assert_eq!(with_chunk, 1);
554        });
555    }
556
557    #[test]
558    fn test_insert_new_first_chunk_gap() {
559        let new_first_chunk = RawChunk {
560            previous: None,
561            identifier: ChunkIdentifier::new(1),
562            next: Some(ChunkIdentifier::new(0)),
563            content: ChunkContent::Gap(()),
564        };
565
566        let mut linked_chunk = LinkedChunk::<5, char, ()>::new_with_update_history();
567        linked_chunk.push_items_back(vec!['a', 'b']);
568        linked_chunk.links.first_chunk_mut().lazy_previous = Some(ChunkIdentifier::new(1));
569
570        // Drain initial updates.
571        let _ = linked_chunk.updates().unwrap().take();
572
573        insert_new_first_chunk(&mut linked_chunk, new_first_chunk).unwrap();
574
575        // Iterate forwards to ensure forwards links are okay.
576        {
577            let mut chunks = linked_chunk.chunks();
578
579            assert_matches!(chunks.next(), Some(chunk) => {
580                assert_eq!(chunk.identifier(), 1);
581                assert!(chunk.is_gap());
582            });
583            assert_matches!(chunks.next(), Some(chunk) => {
584                assert_eq!(chunk.identifier(), 0);
585                assert!(chunk.is_items());
586            });
587            assert!(chunks.next().is_none());
588        }
589
590        // Iterate backwards to ensure backwards links are okay.
591        {
592            let mut rchunks = linked_chunk.rchunks();
593
594            assert_eq!(rchunks.next().unwrap().identifier(), 0);
595            assert_eq!(rchunks.next().unwrap().identifier(), 1);
596            assert!(rchunks.next().is_none());
597        }
598
599        // Check updates.
600        {
601            let updates = linked_chunk.updates().unwrap().take();
602
603            assert_eq!(updates.len(), 1);
604            assert_eq!(
605                updates,
606                [Update::NewGapChunk {
607                    previous: None,
608                    new: ChunkIdentifier::new(1),
609                    next: Some(ChunkIdentifier::new(0)),
610                    gap: (),
611                }]
612            );
613        }
614    }
615
616    #[test]
617    fn test_insert_new_first_chunk_items() {
618        let new_first_chunk = RawChunk {
619            previous: None,
620            identifier: ChunkIdentifier::new(1),
621            next: Some(ChunkIdentifier::new(0)),
622            content: ChunkContent::Items(vec!['c', 'd']),
623        };
624
625        let mut linked_chunk = LinkedChunk::<5, char, ()>::new_with_update_history();
626        linked_chunk.push_items_back(vec!['a', 'b']);
627        linked_chunk.links.first_chunk_mut().lazy_previous = Some(ChunkIdentifier::new(1));
628
629        // Drain initial updates.
630        let _ = linked_chunk.updates().unwrap().take();
631
632        insert_new_first_chunk(&mut linked_chunk, new_first_chunk).unwrap();
633
634        // Iterate forwards to ensure forwards links are okay.
635        {
636            let mut chunks = linked_chunk.chunks();
637
638            assert_matches!(chunks.next(), Some(chunk) => {
639                assert_eq!(chunk.identifier(), 1);
640                assert!(chunk.is_items());
641            });
642            assert_matches!(chunks.next(), Some(chunk) => {
643                assert_eq!(chunk.identifier(), 0);
644                assert!(chunk.is_items());
645            });
646            assert!(chunks.next().is_none());
647        }
648
649        // Iterate backwards to ensure backwards links are okay.
650        {
651            let mut rchunks = linked_chunk.rchunks();
652
653            assert_eq!(rchunks.next().unwrap().identifier(), 0);
654            assert_eq!(rchunks.next().unwrap().identifier(), 1);
655            assert!(rchunks.next().is_none());
656        }
657
658        // Check updates.
659        {
660            let updates = linked_chunk.updates().unwrap().take();
661
662            assert_eq!(updates.len(), 2);
663            assert_eq!(
664                updates,
665                [
666                    Update::NewItemsChunk {
667                        previous: None,
668                        new: ChunkIdentifier::new(1),
669                        next: Some(ChunkIdentifier::new(0)),
670                    },
671                    Update::PushItems {
672                        at: Position::new(ChunkIdentifier::new(1), 0),
673                        items: vec!['c', 'd']
674                    }
675                ]
676            );
677        }
678    }
679
680    #[test]
681    fn test_replace_with_chunk_too_large() {
682        // Start with a linked chunk with 3 chunks: one item, one gap, one item.
683        let mut linked_chunk = LinkedChunk::<2, char, ()>::new();
684        linked_chunk.push_items_back(vec!['a', 'b']);
685        linked_chunk.push_gap_back(());
686        linked_chunk.push_items_back(vec!['c', 'd']);
687
688        // Try to replace it with a last chunk that has too many items.
689        let chunk_identifier_generator = ChunkIdentifierGenerator::new_from_scratch();
690
691        let chunk_id = ChunkIdentifier::new(1);
692        let raw_chunk = RawChunk {
693            previous: Some(ChunkIdentifier::new(0)),
694            identifier: chunk_id,
695            next: None,
696            content: ChunkContent::Items(vec!['e', 'f', 'g', 'h']),
697        };
698
699        let err = replace_with(&mut linked_chunk, Some(raw_chunk), chunk_identifier_generator)
700            .unwrap_err();
701        assert_matches!(err, LazyLoaderError::ChunkTooLarge { id } => {
702            assert_eq!(chunk_id, id);
703        });
704    }
705
706    #[test]
707    fn test_replace_with_next_chunk() {
708        // Start with a linked chunk with 3 chunks: one item, one gap, one item.
709        let mut linked_chunk = LinkedChunk::<2, char, ()>::new();
710        linked_chunk.push_items_back(vec!['a', 'b']);
711        linked_chunk.push_gap_back(());
712        linked_chunk.push_items_back(vec!['c', 'd']);
713
714        // Try to replace it with a last chunk that has too many items.
715        let chunk_identifier_generator = ChunkIdentifierGenerator::new_from_scratch();
716
717        let chunk_id = ChunkIdentifier::new(1);
718        let raw_chunk = RawChunk {
719            previous: Some(ChunkIdentifier::new(0)),
720            identifier: chunk_id,
721            next: Some(ChunkIdentifier::new(2)),
722            content: ChunkContent::Items(vec!['e', 'f']),
723        };
724
725        let err = replace_with(&mut linked_chunk, Some(raw_chunk), chunk_identifier_generator)
726            .unwrap_err();
727        assert_matches!(err, LazyLoaderError::ChunkIsNotLast { id } => {
728            assert_eq!(chunk_id, id);
729        });
730    }
731
732    #[test]
733    fn test_replace_with_empty() {
734        // Start with a linked chunk with 3 chunks: one item, one gap, one item.
735        let mut linked_chunk = LinkedChunk::<2, char, ()>::new_with_update_history();
736        linked_chunk.push_items_back(vec!['a', 'b']);
737        linked_chunk.push_gap_back(());
738        linked_chunk.push_items_back(vec!['c', 'd']);
739
740        // Drain initial updates.
741        let _ = linked_chunk.updates().unwrap().take();
742
743        // Replace it with… you know, nothing (jon snow).
744        let chunk_identifier_generator =
745            ChunkIdentifierGenerator::new_from_previous_chunk_identifier(
746                ChunkIdentifierGenerator::FIRST_IDENTIFIER,
747            );
748        replace_with(&mut linked_chunk, None, chunk_identifier_generator).unwrap();
749
750        // The linked chunk still has updates enabled.
751        assert!(linked_chunk.updates().is_some());
752
753        // Check the linked chunk only contains the default empty events chunk.
754        let mut it = linked_chunk.chunks();
755
756        assert_matches!(it.next(), Some(chunk) => {
757            assert_eq!(chunk.identifier(), ChunkIdentifier::new(0));
758            assert!(chunk.is_items());
759            assert!(chunk.next().is_none());
760            assert_matches!(chunk.content(), ChunkContent::Items(items) => {
761                assert!(items.is_empty());
762            });
763        });
764
765        // And there's no other chunk.
766        assert_matches!(it.next(), None);
767
768        // Check updates.
769        {
770            let updates = linked_chunk.updates().unwrap().take();
771
772            assert_eq!(updates.len(), 2);
773            assert_eq!(
774                updates,
775                [
776                    Update::Clear,
777                    Update::NewItemsChunk {
778                        previous: None,
779                        new: ChunkIdentifier::new(0),
780                        next: None,
781                    },
782                ]
783            );
784        }
785    }
786
787    #[test]
788    fn test_replace_with_non_empty() {
789        // Start with a linked chunk with 3 chunks: one item, one gap, one item.
790        let mut linked_chunk = LinkedChunk::<2, char, ()>::new_with_update_history();
791        linked_chunk.push_items_back(vec!['a', 'b']);
792        linked_chunk.push_gap_back(());
793        linked_chunk.push_items_back(vec!['c', 'd']);
794
795        // Drain initial updates.
796        let _ = linked_chunk.updates().unwrap().take();
797
798        // Replace it with a single chunk (sorry, jon).
799        let chunk_identifier_generator =
800            ChunkIdentifierGenerator::new_from_previous_chunk_identifier(ChunkIdentifier::new(42));
801
802        let chunk_id = ChunkIdentifier::new(1);
803        let chunk = RawChunk {
804            previous: Some(ChunkIdentifier::new(0)),
805            identifier: chunk_id,
806            next: None,
807            content: ChunkContent::Items(vec!['e', 'f']),
808        };
809        replace_with(&mut linked_chunk, Some(chunk), chunk_identifier_generator).unwrap();
810
811        // The linked chunk still has updates enabled.
812        assert!(linked_chunk.updates().is_some());
813
814        let mut it = linked_chunk.chunks();
815
816        // The first chunk is an event chunks with the expected items.
817        assert_matches!(it.next(), Some(chunk) => {
818            assert_eq!(chunk.identifier(), chunk_id);
819            assert!(chunk.next().is_none());
820            assert_matches!(chunk.content(), ChunkContent::Items(items) => {
821                assert_eq!(*items, vec!['e', 'f']);
822            });
823        });
824
825        // Nothing more.
826        assert!(it.next().is_none());
827
828        // Check updates.
829        {
830            let updates = linked_chunk.updates().unwrap().take();
831
832            assert_eq!(updates.len(), 3);
833            assert_eq!(
834                updates,
835                [
836                    Update::Clear,
837                    Update::NewItemsChunk {
838                        previous: Some(ChunkIdentifier::new(0)),
839                        new: chunk_id,
840                        next: None,
841                    },
842                    Update::PushItems {
843                        at: Position::new(ChunkIdentifier::new(1), 0),
844                        items: vec!['e', 'f']
845                    }
846                ]
847            );
848        }
849    }
850
851    #[test]
852    fn test_from_all_chunks_empty() {
853        // Building an empty linked chunk works, and returns `None`.
854        let lc = from_all_chunks::<3, char, ()>(vec![]).unwrap();
855        assert!(lc.is_none());
856    }
857
858    #[test]
859    fn test_from_all_chunks_success() {
860        let cid0 = ChunkIdentifier::new(0);
861        let cid1 = ChunkIdentifier::new(1);
862        // Note: cid2 is missing on purpose, to confirm that it's fine to have holes in
863        // the chunk id space.
864        let cid3 = ChunkIdentifier::new(3);
865
866        // Check that we can successfully create a linked chunk, independently of the
867        // order in which chunks are added.
868        //
869        // The final chunk will contain [cid0 <-> cid1 <-> cid3], in this order.
870
871        let chunks = vec![
872            // Adding chunk cid0.
873            RawChunk {
874                previous: None,
875                identifier: cid0,
876                next: Some(cid1),
877                content: ChunkContent::Items(vec!['a', 'b', 'c']),
878            },
879            // Adding chunk cid3.
880            RawChunk {
881                previous: Some(cid1),
882                identifier: cid3,
883                next: None,
884                content: ChunkContent::Items(vec!['d', 'e']),
885            },
886            // Adding chunk cid1.
887            RawChunk {
888                previous: Some(cid0),
889                identifier: cid1,
890                next: Some(cid3),
891                content: ChunkContent::Gap('g'),
892            },
893        ];
894
895        let mut lc = from_all_chunks::<3, _, _>(chunks)
896            .expect("building works")
897            .expect("returns a non-empty linked chunk");
898
899        // Check the entire content first.
900        assert_items_eq!(lc, ['a', 'b', 'c'] [-] ['d', 'e']);
901
902        // Run checks on the first chunk.
903        let mut chunks = lc.chunks();
904        let first_chunk = chunks.next().unwrap();
905        {
906            assert!(first_chunk.previous().is_none());
907            assert_eq!(first_chunk.identifier(), cid0);
908        }
909
910        // Run checks on the second chunk.
911        let second_chunk = chunks.next().unwrap();
912        {
913            assert_eq!(second_chunk.identifier(), first_chunk.next().unwrap().identifier());
914            assert_eq!(second_chunk.previous().unwrap().identifier(), first_chunk.identifier());
915            assert_eq!(second_chunk.identifier(), cid1);
916        }
917
918        // Run checks on the third chunk.
919        let third_chunk = chunks.next().unwrap();
920        {
921            assert_eq!(third_chunk.identifier(), second_chunk.next().unwrap().identifier());
922            assert_eq!(third_chunk.previous().unwrap().identifier(), second_chunk.identifier());
923            assert!(third_chunk.next().is_none());
924            assert_eq!(third_chunk.identifier(), cid3);
925        }
926
927        // There's no more chunk.
928        assert!(chunks.next().is_none());
929
930        // The linked chunk had 5 items.
931        assert_eq!(lc.num_items(), 5);
932
933        // Now, if we add a new chunk, its identifier should be the previous one we used
934        // + 1.
935        lc.push_gap_back('h');
936
937        let last_chunk = lc.chunks().last().unwrap();
938        assert_eq!(last_chunk.identifier(), ChunkIdentifier::new(cid3.index() + 1));
939    }
940
941    #[test]
942    fn test_from_all_chunks_chunk_too_large() {
943        let cid0 = ChunkIdentifier::new(0);
944
945        // Adding a chunk with 4 items will fail, because the max capacity specified in
946        // the builder generics is 3.
947        let res = from_all_chunks::<3, char, ()>(vec![RawChunk {
948            previous: None,
949            identifier: cid0,
950            next: None,
951            content: ChunkContent::Items(vec!['a', 'b', 'c', 'd']),
952        }]);
953        assert_matches!(res, Err(LazyLoaderError::ChunkTooLarge { id }) => {
954            assert_eq!(id, cid0);
955        });
956    }
957
958    #[test]
959    fn test_from_all_chunks_missing_first_chunk() {
960        let cid0 = ChunkIdentifier::new(0);
961        let cid1 = ChunkIdentifier::new(1);
962        let cid2 = ChunkIdentifier::new(2);
963
964        let res = from_all_chunks::<3, char, char>(vec![
965            RawChunk {
966                previous: Some(cid2),
967                identifier: cid0,
968                next: Some(cid1),
969                content: ChunkContent::Gap('g'),
970            },
971            RawChunk {
972                previous: Some(cid0),
973                identifier: cid1,
974                next: None,
975                content: ChunkContent::Items(vec!['a', 'b', 'c']),
976            },
977        ]);
978        assert_matches!(res, Err(LazyLoaderError::ChunkIsNotFirst { id }) => {
979            assert_eq!(id, cid0);
980        });
981    }
982
983    #[test]
984    fn test_from_all_chunks_multiple_first_chunks() {
985        let cid0 = ChunkIdentifier::new(0);
986        let cid1 = ChunkIdentifier::new(1);
987
988        let res = from_all_chunks::<3, char, char>(vec![
989            RawChunk {
990                previous: None,
991                identifier: cid0,
992                next: None,
993                content: ChunkContent::Gap('g'),
994            },
995            // Second chunk lies and pretends to be the first too.
996            RawChunk {
997                previous: None,
998                identifier: cid1,
999                next: None,
1000                content: ChunkContent::Gap('G'),
1001            },
1002        ]);
1003
1004        assert_matches!(res, Err(LazyLoaderError::MultipleConnectedComponents));
1005    }
1006
1007    #[test]
1008    fn test_from_all_chunks_cycle() {
1009        let cid0 = ChunkIdentifier::new(0);
1010        let cid1 = ChunkIdentifier::new(1);
1011
1012        let res = from_all_chunks::<3, char, char>(vec![
1013            RawChunk {
1014                previous: None,
1015                identifier: cid0,
1016                next: None,
1017                content: ChunkContent::Gap('g'),
1018            },
1019            RawChunk {
1020                previous: Some(cid0),
1021                identifier: cid1,
1022                next: Some(cid0),
1023                content: ChunkContent::Gap('G'),
1024            },
1025        ]);
1026
1027        assert_matches!(res, Err(LazyLoaderError::Cycle { new_chunk, with_chunk }) => {
1028            assert_eq!(new_chunk, cid1);
1029            assert_eq!(with_chunk, cid0);
1030        });
1031    }
1032
1033    #[test]
1034    fn test_from_all_chunks_multiple_connected_components() {
1035        let cid0 = ChunkIdentifier::new(0);
1036        let cid1 = ChunkIdentifier::new(1);
1037        let cid2 = ChunkIdentifier::new(2);
1038
1039        let res = from_all_chunks::<3, char, char>(vec![
1040            // cid0 and cid1 are linked to each other.
1041            RawChunk {
1042                previous: None,
1043                identifier: cid0,
1044                next: Some(cid1),
1045                content: ChunkContent::Gap('g'),
1046            },
1047            RawChunk {
1048                previous: Some(cid0),
1049                identifier: cid1,
1050                next: None,
1051                content: ChunkContent::Gap('G'),
1052            },
1053            // cid2 stands on its own.
1054            RawChunk {
1055                previous: None,
1056                identifier: cid2,
1057                next: None,
1058                content: ChunkContent::Gap('h'),
1059            },
1060        ]);
1061
1062        assert_matches!(res, Err(LazyLoaderError::MultipleConnectedComponents));
1063    }
1064}