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 expected_next_chunk = linked_chunk.links.first_chunk().identifier();
100
101        // New chunk has a next chunk.
102        let Some(next_chunk) = new_first_chunk.next else {
103            return Err(LazyLoaderError::MissingNextChunk { id: new_first_chunk.identifier });
104        };
105
106        // New chunk has a next chunk, and it is the first chunk of the `LinkedChunk`.
107        if next_chunk != expected_next_chunk {
108            return Err(LazyLoaderError::CannotConnectTwoChunks {
109                new_chunk: new_first_chunk.identifier,
110                with_chunk: expected_next_chunk,
111            });
112        }
113
114        // Alright. All checks are made.
115    }
116
117    // Insert the new first chunk.
118    {
119        // Transform the `RawChunk` into a `Chunk`.
120        let lazy_previous = new_first_chunk.previous.take();
121        let mut new_first_chunk =
122            Chunk::new_leaked(new_first_chunk.identifier, new_first_chunk.content);
123
124        let links = &mut linked_chunk.links;
125
126        // Update the first chunk.
127        {
128            let first_chunk = links.first_chunk_mut();
129
130            debug_assert!(
131                first_chunk.previous.is_none(),
132                "The first chunk is not supposed to have a previous chunk"
133            );
134
135            // Move the `lazy_previous` if any.
136            first_chunk.lazy_previous = None;
137            unsafe { new_first_chunk.as_mut() }.lazy_previous = lazy_previous;
138
139            // Link one way: `new_first_chunk` becomes the previous chunk of the first
140            // chunk.
141            first_chunk.previous = Some(new_first_chunk);
142        }
143
144        // Update `links`.
145        {
146            // Remember the pointer to the `first_chunk`.
147            let old_first_chunk = links.first;
148
149            // `new_first_chunk` becomes the new first chunk.
150            links.first = new_first_chunk;
151
152            // Link the other way: `old_first_chunk` becomes the next chunk of the first
153            // chunk.
154            links.first_chunk_mut().next = Some(old_first_chunk);
155
156            debug_assert!(
157                links.first_chunk().previous.is_none(),
158                "The new first chunk is not supposed to have a previous chunk"
159            );
160
161            // Update the last chunk. If it's `Some(_)`, no need to update the last chunk
162            // pointer. If it's `None`, it means we had only one chunk; now we have two, the
163            // last chunk is the `old_first_chunk`.
164            if links.last.is_none() {
165                links.last = Some(old_first_chunk);
166            }
167        }
168    }
169
170    // Emit the updates.
171    if let Some(updates) = linked_chunk.updates.as_mut() {
172        let first_chunk = linked_chunk.links.first_chunk();
173
174        let previous = first_chunk.previous().map(Chunk::identifier).or(first_chunk.lazy_previous);
175        let new = first_chunk.identifier();
176        let next = first_chunk.next().map(Chunk::identifier);
177
178        match first_chunk.content() {
179            ChunkContent::Gap(gap) => {
180                updates.push(Update::NewGapChunk { previous, new, next, gap: gap.clone() });
181            }
182
183            ChunkContent::Items(items) => {
184                updates.push(Update::NewItemsChunk { previous, new, next });
185                updates.push(Update::PushItems {
186                    at: first_chunk.first_position(),
187                    items: items.clone(),
188                });
189            }
190        }
191    }
192
193    Ok(())
194}
195
196/// A pretty inefficient, test-only, function to rebuild a full `LinkedChunk`.
197#[doc(hidden)]
198pub fn from_all_chunks<const CAP: usize, Item, Gap>(
199    mut chunks: Vec<RawChunk<Item, Gap>>,
200) -> Result<Option<LinkedChunk<CAP, Item, Gap>>, LazyLoaderError>
201where
202    Item: Clone,
203    Gap: Clone,
204{
205    if chunks.is_empty() {
206        return Ok(None);
207    }
208
209    // Sort by `next` so that the search for the next chunk is faster (it should
210    // come first). The chunk with the biggest next chunk identifier comes first.
211    // Chunk with no next chunk comes last.
212    chunks.sort_by(|a, b| b.next.cmp(&a.next));
213
214    let last_chunk = chunks
215        .pop()
216        // SAFETY: `chunks` is guaranteed to not be empty, `pop` cannot fail.
217        .expect("`chunks` is supposed to not be empty, we must be able to `pop` an item");
218    let last_chunk_identifier = last_chunk.identifier;
219    let chunk_identifier_generator =
220        ChunkIdentifierGenerator::new_from_previous_chunk_identifier(last_chunk_identifier);
221
222    let Some(mut linked_chunk) = from_last_chunk(Some(last_chunk), chunk_identifier_generator)?
223    else {
224        return Ok(None);
225    };
226
227    let mut next_chunk = last_chunk_identifier;
228
229    while let Some(chunk) = chunks
230        .iter()
231        .position(|chunk| chunk.next == Some(next_chunk))
232        .map(|index| chunks.remove(index))
233    {
234        next_chunk = chunk.identifier;
235        insert_new_first_chunk(&mut linked_chunk, chunk)?;
236    }
237
238    let first_chunk = linked_chunk.links.first_chunk();
239
240    // It is expected that **all chunks** are passed to this function. If there was
241    // a previous chunk, `insert_new_first_chunk` has erased it and moved it to
242    // `lazy_previous`. Hence, let's check both (the former condition isn't
243    // necessary, but better be robust).
244    if first_chunk.previous().is_some() || first_chunk.lazy_previous.is_some() {
245        return Err(LazyLoaderError::ChunkIsNotFirst { id: first_chunk.identifier() });
246    }
247
248    if !chunks.is_empty() {
249        return Err(LazyLoaderError::MultipleConnectedComponents);
250    }
251
252    Ok(Some(linked_chunk))
253}
254
255#[derive(thiserror::Error, Debug)]
256pub enum LazyLoaderError {
257    #[error("chunk with id {} has a next chunk, it is supposed to be the last chunk", id.index())]
258    ChunkIsNotLast { id: ChunkIdentifier },
259
260    #[error("chunk with id {} forms a cycle with chunk with id {}", new_chunk.index(), with_chunk.index())]
261    Cycle { new_chunk: ChunkIdentifier, with_chunk: ChunkIdentifier },
262
263    #[error("chunk with id {} is supposed to have a next chunk", id.index())]
264    MissingNextChunk { id: ChunkIdentifier },
265
266    #[error(
267        "chunk with id {} cannot be connected to chunk with id {} because the identifiers do not match",
268        new_chunk.index(),
269        with_chunk.index()
270    )]
271    CannotConnectTwoChunks { new_chunk: ChunkIdentifier, with_chunk: ChunkIdentifier },
272
273    #[error("chunk with id {} is too large", id.index())]
274    ChunkTooLarge { id: ChunkIdentifier },
275
276    #[doc(hidden)]
277    #[error("the last chunk is missing")]
278    MissingLastChunk,
279
280    #[doc(hidden)]
281    #[error("chunk with id {} has a previous chunk, it is supposed to be the first chunk", id.index())]
282    ChunkIsNotFirst { id: ChunkIdentifier },
283
284    #[doc(hidden)]
285    #[error("multiple connected components")]
286    MultipleConnectedComponents,
287}
288
289#[cfg(test)]
290mod tests {
291    use assert_matches::assert_matches;
292
293    use super::{
294        super::Position, from_all_chunks, from_last_chunk, insert_new_first_chunk, ChunkContent,
295        ChunkIdentifier, ChunkIdentifierGenerator, LazyLoaderError, LinkedChunk, RawChunk, Update,
296    };
297
298    #[test]
299    fn test_from_last_chunk_err_too_much_items() {
300        let last_chunk = RawChunk {
301            previous: None,
302            identifier: ChunkIdentifier::new(0),
303            next: None,
304            content: ChunkContent::Items(vec!['a', 'b', 'c']),
305        };
306        let chunk_identifier_generator =
307            ChunkIdentifierGenerator::new_from_previous_chunk_identifier(ChunkIdentifier::new(0));
308
309        let maybe_linked_chunk =
310            from_last_chunk::<2, char, ()>(Some(last_chunk), chunk_identifier_generator);
311
312        assert_matches!(
313            maybe_linked_chunk,
314            Err(LazyLoaderError::ChunkTooLarge { id }) => {
315                assert_eq!(id, 0);
316            }
317        );
318    }
319
320    #[test]
321    fn test_from_last_chunk_err_is_not_last_chunk() {
322        let last_chunk = RawChunk {
323            previous: None,
324            identifier: ChunkIdentifier::new(0),
325            next: Some(ChunkIdentifier::new(42)),
326            content: ChunkContent::Items(vec!['a']),
327        };
328        let chunk_identifier_generator =
329            ChunkIdentifierGenerator::new_from_previous_chunk_identifier(ChunkIdentifier::new(0));
330
331        let maybe_linked_chunk =
332            from_last_chunk::<2, char, ()>(Some(last_chunk), chunk_identifier_generator);
333
334        assert_matches!(
335            maybe_linked_chunk,
336            Err(LazyLoaderError::ChunkIsNotLast { id }) => {
337                assert_eq!(id, 0);
338            }
339        );
340    }
341
342    #[test]
343    fn test_from_last_chunk_none() {
344        let chunk_identifier_generator =
345            ChunkIdentifierGenerator::new_from_previous_chunk_identifier(ChunkIdentifier::new(0));
346
347        let maybe_linked_chunk =
348            from_last_chunk::<2, char, ()>(None, chunk_identifier_generator).unwrap();
349
350        assert!(maybe_linked_chunk.is_none());
351    }
352
353    #[test]
354    fn test_from_last_chunk() {
355        let last_chunk = RawChunk {
356            previous: Some(ChunkIdentifier::new(42)),
357            identifier: ChunkIdentifier::new(0),
358            next: None,
359            content: ChunkContent::Items(vec!['a']),
360        };
361        let chunk_identifier_generator =
362            ChunkIdentifierGenerator::new_from_previous_chunk_identifier(ChunkIdentifier::new(0));
363
364        let maybe_linked_chunk =
365            from_last_chunk::<2, char, ()>(Some(last_chunk), chunk_identifier_generator).unwrap();
366
367        assert_matches!(maybe_linked_chunk, Some(mut linked_chunk) => {
368            let mut chunks = linked_chunk.chunks();
369
370            assert_matches!(chunks.next(), Some(chunk) => {
371                assert_eq!(chunk.identifier(), 0);
372                // The chunk's previous has been set to `None`
373                assert!(chunk.previous().is_none());
374            });
375            assert!(chunks.next().is_none());
376
377            // It has updates enabled.
378            assert!(linked_chunk.updates().is_some());
379        });
380    }
381
382    #[test]
383    fn test_insert_new_first_chunk_err_too_much_items() {
384        let new_first_chunk = RawChunk {
385            previous: None,
386            identifier: ChunkIdentifier::new(0),
387            next: None,
388            content: ChunkContent::Items(vec!['a', 'b', 'c']),
389        };
390
391        let mut linked_chunk = LinkedChunk::<2, char, ()>::new();
392
393        let result = insert_new_first_chunk(&mut linked_chunk, new_first_chunk);
394
395        assert_matches!(result, Err(LazyLoaderError::ChunkTooLarge { id }) => {
396            assert_eq!(id, 0);
397        });
398    }
399
400    #[test]
401    fn test_insert_new_first_chunk_err_cycle() {
402        let new_first_chunk = RawChunk {
403            previous: Some(ChunkIdentifier::new(0)),
404            identifier: ChunkIdentifier::new(1),
405            next: Some(ChunkIdentifier::new(0)),
406            content: ChunkContent::Gap(()),
407        };
408
409        let mut linked_chunk = LinkedChunk::<2, char, ()>::new();
410        let result = insert_new_first_chunk(&mut linked_chunk, new_first_chunk);
411
412        assert_matches!(result, Err(LazyLoaderError::Cycle { new_chunk, with_chunk }) => {
413            assert_eq!(new_chunk, 1);
414            assert_eq!(with_chunk, 0);
415        });
416    }
417
418    #[test]
419    fn test_insert_new_first_chunk_err_missing_next_chunk() {
420        let new_first_chunk = RawChunk {
421            previous: None,
422            identifier: ChunkIdentifier::new(0),
423            next: None,
424            content: ChunkContent::Gap(()),
425        };
426
427        let mut linked_chunk = LinkedChunk::<2, char, ()>::new();
428
429        let result = insert_new_first_chunk(&mut linked_chunk, new_first_chunk);
430
431        assert_matches!(result, Err(LazyLoaderError::MissingNextChunk { id }) => {
432            assert_eq!(id, 0);
433        });
434    }
435
436    #[test]
437    fn test_insert_new_first_chunk_err_cannot_connect_two_chunks() {
438        let new_first_chunk = RawChunk {
439            previous: None,
440            identifier: ChunkIdentifier::new(1),
441            next: Some(ChunkIdentifier::new(42)),
442            content: ChunkContent::Gap(()),
443        };
444
445        let mut linked_chunk = LinkedChunk::<2, char, ()>::new();
446        linked_chunk.push_gap_back(());
447
448        let result = insert_new_first_chunk(&mut linked_chunk, new_first_chunk);
449
450        assert_matches!(result, Err(LazyLoaderError::CannotConnectTwoChunks { new_chunk, with_chunk }) => {
451            assert_eq!(new_chunk, 1);
452            assert_eq!(with_chunk, 0);
453        });
454    }
455
456    #[test]
457    fn test_insert_new_first_chunk_gap() {
458        let new_first_chunk = RawChunk {
459            previous: None,
460            identifier: ChunkIdentifier::new(1),
461            next: Some(ChunkIdentifier::new(0)),
462            content: ChunkContent::Gap(()),
463        };
464
465        let mut linked_chunk = LinkedChunk::<5, char, ()>::new_with_update_history();
466        linked_chunk.push_items_back(vec!['a', 'b']);
467
468        // Drain initial updates.
469        let _ = linked_chunk.updates().unwrap().take();
470
471        insert_new_first_chunk(&mut linked_chunk, new_first_chunk).unwrap();
472
473        // Iterate forwards to ensure forwards links are okay.
474        {
475            let mut chunks = linked_chunk.chunks();
476
477            assert_matches!(chunks.next(), Some(chunk) => {
478                assert_eq!(chunk.identifier(), 1);
479                assert!(chunk.is_gap());
480            });
481            assert_matches!(chunks.next(), Some(chunk) => {
482                assert_eq!(chunk.identifier(), 0);
483                assert!(chunk.is_items());
484            });
485            assert!(chunks.next().is_none());
486        }
487
488        // Iterate backwards to ensure backwards links are okay.
489        {
490            let mut rchunks = linked_chunk.rchunks();
491
492            assert_eq!(rchunks.next().unwrap().identifier(), 0);
493            assert_eq!(rchunks.next().unwrap().identifier(), 1);
494            assert!(rchunks.next().is_none());
495        }
496
497        // Check updates.
498        {
499            let updates = linked_chunk.updates().unwrap().take();
500
501            assert_eq!(updates.len(), 1);
502            assert_eq!(
503                updates,
504                [Update::NewGapChunk {
505                    previous: None,
506                    new: ChunkIdentifier::new(1),
507                    next: Some(ChunkIdentifier::new(0)),
508                    gap: (),
509                }]
510            );
511        }
512    }
513
514    #[test]
515    fn test_insert_new_first_chunk_items() {
516        let new_first_chunk = RawChunk {
517            previous: None,
518            identifier: ChunkIdentifier::new(1),
519            next: Some(ChunkIdentifier::new(0)),
520            content: ChunkContent::Items(vec!['c', 'd']),
521        };
522
523        let mut linked_chunk = LinkedChunk::<5, char, ()>::new_with_update_history();
524        linked_chunk.push_items_back(vec!['a', 'b']);
525
526        // Drain initial updates.
527        let _ = linked_chunk.updates().unwrap().take();
528
529        insert_new_first_chunk(&mut linked_chunk, new_first_chunk).unwrap();
530
531        // Iterate forwards to ensure forwards links are okay.
532        {
533            let mut chunks = linked_chunk.chunks();
534
535            assert_matches!(chunks.next(), Some(chunk) => {
536                assert_eq!(chunk.identifier(), 1);
537                assert!(chunk.is_items());
538            });
539            assert_matches!(chunks.next(), Some(chunk) => {
540                assert_eq!(chunk.identifier(), 0);
541                assert!(chunk.is_items());
542            });
543            assert!(chunks.next().is_none());
544        }
545
546        // Iterate backwards to ensure backwards links are okay.
547        {
548            let mut rchunks = linked_chunk.rchunks();
549
550            assert_eq!(rchunks.next().unwrap().identifier(), 0);
551            assert_eq!(rchunks.next().unwrap().identifier(), 1);
552            assert!(rchunks.next().is_none());
553        }
554
555        // Check updates.
556        {
557            let updates = linked_chunk.updates().unwrap().take();
558
559            assert_eq!(updates.len(), 2);
560            assert_eq!(
561                updates,
562                [
563                    Update::NewItemsChunk {
564                        previous: None,
565                        new: ChunkIdentifier::new(1),
566                        next: Some(ChunkIdentifier::new(0)),
567                    },
568                    Update::PushItems {
569                        at: Position::new(ChunkIdentifier::new(1), 0),
570                        items: vec!['c', 'd']
571                    }
572                ]
573            );
574        }
575    }
576
577    #[test]
578    fn test_from_all_chunks_empty() {
579        // Building an empty linked chunk works, and returns `None`.
580        let lc = from_all_chunks::<3, char, ()>(vec![]).unwrap();
581        assert!(lc.is_none());
582    }
583
584    #[test]
585    fn test_from_all_chunks_success() {
586        let cid0 = ChunkIdentifier::new(0);
587        let cid1 = ChunkIdentifier::new(1);
588        // Note: cid2 is missing on purpose, to confirm that it's fine to have holes in
589        // the chunk id space.
590        let cid3 = ChunkIdentifier::new(3);
591
592        // Check that we can successfully create a linked chunk, independently of the
593        // order in which chunks are added.
594        //
595        // The final chunk will contain [cid0 <-> cid1 <-> cid3], in this order.
596
597        let chunks = vec![
598            // Adding chunk cid0.
599            RawChunk {
600                previous: None,
601                identifier: cid0,
602                next: Some(cid1),
603                content: ChunkContent::Items(vec!['a', 'b', 'c']),
604            },
605            // Adding chunk cid3.
606            RawChunk {
607                previous: Some(cid1),
608                identifier: cid3,
609                next: None,
610                content: ChunkContent::Items(vec!['d', 'e']),
611            },
612            // Adding chunk cid1.
613            RawChunk {
614                previous: Some(cid0),
615                identifier: cid1,
616                next: Some(cid3),
617                content: ChunkContent::Gap('g'),
618            },
619        ];
620
621        let mut lc = from_all_chunks::<3, _, _>(chunks)
622            .expect("building works")
623            .expect("returns a non-empty linked chunk");
624
625        // Check the entire content first.
626        assert_items_eq!(lc, ['a', 'b', 'c'] [-] ['d', 'e']);
627
628        // Run checks on the first chunk.
629        let mut chunks = lc.chunks();
630        let first_chunk = chunks.next().unwrap();
631        {
632            assert!(first_chunk.previous().is_none());
633            assert_eq!(first_chunk.identifier(), cid0);
634        }
635
636        // Run checks on the second chunk.
637        let second_chunk = chunks.next().unwrap();
638        {
639            assert_eq!(second_chunk.identifier(), first_chunk.next().unwrap().identifier());
640            assert_eq!(second_chunk.previous().unwrap().identifier(), first_chunk.identifier());
641            assert_eq!(second_chunk.identifier(), cid1);
642        }
643
644        // Run checks on the third chunk.
645        let third_chunk = chunks.next().unwrap();
646        {
647            assert_eq!(third_chunk.identifier(), second_chunk.next().unwrap().identifier());
648            assert_eq!(third_chunk.previous().unwrap().identifier(), second_chunk.identifier());
649            assert!(third_chunk.next().is_none());
650            assert_eq!(third_chunk.identifier(), cid3);
651        }
652
653        // There's no more chunk.
654        assert!(chunks.next().is_none());
655
656        // The linked chunk had 5 items.
657        assert_eq!(lc.num_items(), 5);
658
659        // Now, if we add a new chunk, its identifier should be the previous one we used
660        // + 1.
661        lc.push_gap_back('h');
662
663        let last_chunk = lc.chunks().last().unwrap();
664        assert_eq!(last_chunk.identifier(), ChunkIdentifier::new(cid3.index() + 1));
665    }
666
667    #[test]
668    fn test_from_all_chunks_chunk_too_large() {
669        let cid0 = ChunkIdentifier::new(0);
670
671        // Adding a chunk with 4 items will fail, because the max capacity specified in
672        // the builder generics is 3.
673        let res = from_all_chunks::<3, char, ()>(vec![RawChunk {
674            previous: None,
675            identifier: cid0,
676            next: None,
677            content: ChunkContent::Items(vec!['a', 'b', 'c', 'd']),
678        }]);
679        assert_matches!(res, Err(LazyLoaderError::ChunkTooLarge { id }) => {
680            assert_eq!(id, cid0);
681        });
682    }
683
684    #[test]
685    fn test_from_all_chunks_missing_first_chunk() {
686        let cid0 = ChunkIdentifier::new(0);
687        let cid1 = ChunkIdentifier::new(1);
688        let cid2 = ChunkIdentifier::new(2);
689
690        let res = from_all_chunks::<3, char, char>(vec![
691            RawChunk {
692                previous: Some(cid2),
693                identifier: cid0,
694                next: Some(cid1),
695                content: ChunkContent::Gap('g'),
696            },
697            RawChunk {
698                previous: Some(cid0),
699                identifier: cid1,
700                next: None,
701                content: ChunkContent::Items(vec!['a', 'b', 'c']),
702            },
703        ]);
704        assert_matches!(res, Err(LazyLoaderError::ChunkIsNotFirst { id }) => {
705            assert_eq!(id, cid0);
706        });
707    }
708
709    #[test]
710    fn test_from_all_chunks_multiple_first_chunks() {
711        let cid0 = ChunkIdentifier::new(0);
712        let cid1 = ChunkIdentifier::new(1);
713
714        let res = from_all_chunks::<3, char, char>(vec![
715            RawChunk {
716                previous: None,
717                identifier: cid0,
718                next: None,
719                content: ChunkContent::Gap('g'),
720            },
721            // Second chunk lies and pretends to be the first too.
722            RawChunk {
723                previous: None,
724                identifier: cid1,
725                next: None,
726                content: ChunkContent::Gap('G'),
727            },
728        ]);
729
730        assert_matches!(res, Err(LazyLoaderError::MultipleConnectedComponents));
731    }
732
733    #[test]
734    fn test_from_all_chunks_cycle() {
735        let cid0 = ChunkIdentifier::new(0);
736        let cid1 = ChunkIdentifier::new(1);
737
738        let res = from_all_chunks::<3, char, char>(vec![
739            RawChunk {
740                previous: None,
741                identifier: cid0,
742                next: None,
743                content: ChunkContent::Gap('g'),
744            },
745            RawChunk {
746                previous: Some(cid0),
747                identifier: cid1,
748                next: Some(cid0),
749                content: ChunkContent::Gap('G'),
750            },
751        ]);
752
753        assert_matches!(res, Err(LazyLoaderError::Cycle { new_chunk, with_chunk }) => {
754            assert_eq!(new_chunk, cid1);
755            assert_eq!(with_chunk, cid0);
756        });
757    }
758
759    #[test]
760    fn test_from_all_chunks_multiple_connected_components() {
761        let cid0 = ChunkIdentifier::new(0);
762        let cid1 = ChunkIdentifier::new(1);
763        let cid2 = ChunkIdentifier::new(2);
764
765        let res = from_all_chunks::<3, char, char>(vec![
766            // cid0 and cid1 are linked to each other.
767            RawChunk {
768                previous: None,
769                identifier: cid0,
770                next: Some(cid1),
771                content: ChunkContent::Gap('g'),
772            },
773            RawChunk {
774                previous: Some(cid0),
775                identifier: cid1,
776                next: None,
777                content: ChunkContent::Gap('G'),
778            },
779            // cid2 stands on its own.
780            RawChunk {
781                previous: None,
782                identifier: cid2,
783                next: None,
784                content: ChunkContent::Gap('h'),
785            },
786        ]);
787
788        assert_matches!(res, Err(LazyLoaderError::MultipleConnectedComponents));
789    }
790}