matrix_sdk/event_cache/room/
events.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 as_variant::as_variant;
16use eyeball_im::VectorDiff;
17pub use matrix_sdk_base::event_cache::{Event, Gap};
18use matrix_sdk_base::{
19    event_cache::store::DEFAULT_CHUNK_CAPACITY,
20    linked_chunk::{
21        lazy_loader::{self, LazyLoaderError},
22        ChunkContent, ChunkIdentifierGenerator, RawChunk,
23    },
24};
25use matrix_sdk_common::linked_chunk::{
26    AsVector, Chunk, ChunkIdentifier, Error, Iter, IterBackward, LinkedChunk, ObservableUpdates,
27    Position,
28};
29
30/// This type represents all events of a single room.
31#[derive(Debug)]
32pub struct RoomEvents {
33    /// The real in-memory storage for all the events.
34    chunks: LinkedChunk<DEFAULT_CHUNK_CAPACITY, Event, Gap>,
35
36    /// Type mapping [`Update`]s from [`Self::chunks`] to [`VectorDiff`]s.
37    ///
38    /// [`Update`]: matrix_sdk_base::linked_chunk::Update
39    chunks_updates_as_vectordiffs: AsVector<Event, Gap>,
40}
41
42impl Default for RoomEvents {
43    fn default() -> Self {
44        Self::new()
45    }
46}
47
48impl RoomEvents {
49    /// Build a new [`RoomEvents`] struct with zero events.
50    pub fn new() -> Self {
51        Self::with_initial_linked_chunk(None)
52    }
53
54    /// Build a new [`RoomEvents`] struct with prior chunks knowledge.
55    ///
56    /// The provided [`LinkedChunk`] must have been built with update history.
57    pub fn with_initial_linked_chunk(
58        linked_chunk: Option<LinkedChunk<DEFAULT_CHUNK_CAPACITY, Event, Gap>>,
59    ) -> Self {
60        let mut linked_chunk = linked_chunk.unwrap_or_else(LinkedChunk::new_with_update_history);
61
62        let chunks_updates_as_vectordiffs = linked_chunk
63            .as_vector()
64            // SAFETY: The `LinkedChunk` has been built with `new_with_update_history`, so
65            // `as_vector` must return `Some(…)`.
66            .expect("`LinkedChunk` must have been built with `new_with_update_history`");
67
68        Self { chunks: linked_chunk, chunks_updates_as_vectordiffs }
69    }
70
71    /// Clear all events.
72    ///
73    /// All events, all gaps, everything is dropped, move into the void, into
74    /// the ether, forever.
75    pub fn reset(&mut self) {
76        self.chunks.clear();
77    }
78
79    /// Replace the events with the given last chunk of events and generator.
80    ///
81    /// This clears all the chunks in memory before resetting to the new chunk,
82    /// if provided.
83    pub(super) fn replace_with(
84        &mut self,
85        last_chunk: Option<RawChunk<Event, Gap>>,
86        chunk_identifier_generator: ChunkIdentifierGenerator,
87    ) -> Result<(), LazyLoaderError> {
88        lazy_loader::replace_with(&mut self.chunks, last_chunk, chunk_identifier_generator)
89    }
90
91    /// Push events after all events or gaps.
92    ///
93    /// The last event in `events` is the most recent one.
94    pub fn push_events<I>(&mut self, events: I)
95    where
96        I: IntoIterator<Item = Event>,
97        I::IntoIter: ExactSizeIterator,
98    {
99        self.chunks.push_items_back(events);
100    }
101
102    /// Push a gap after all events or gaps.
103    pub fn push_gap(&mut self, gap: Gap) {
104        self.chunks.push_gap_back(gap);
105    }
106
107    /// Insert events at a specified position.
108    pub fn insert_events_at(
109        &mut self,
110        events: Vec<Event>,
111        position: Position,
112    ) -> Result<(), Error> {
113        self.chunks.insert_items_at(events, position)?;
114        Ok(())
115    }
116
117    /// Insert a gap at a specified position.
118    pub fn insert_gap_at(&mut self, gap: Gap, position: Position) -> Result<(), Error> {
119        self.chunks.insert_gap_at(gap, position)
120    }
121
122    /// Remove an empty chunk at the given position.
123    ///
124    /// Note: the chunk must either be a gap, or an empty items chunk, and it
125    /// must NOT be the last one.
126    ///
127    /// Returns the next insert position, if any, left after the chunk that has
128    /// just been removed.
129    pub fn remove_empty_chunk_at(
130        &mut self,
131        gap: ChunkIdentifier,
132    ) -> Result<Option<Position>, Error> {
133        self.chunks.remove_empty_chunk_at(gap)
134    }
135
136    /// Replace the gap identified by `gap_identifier`, by events.
137    ///
138    /// Because the `gap_identifier` can represent non-gap chunk, this method
139    /// returns a `Result`.
140    ///
141    /// This method returns the position of the (first if many) newly created
142    /// `Chunk` that contains the `items`.
143    pub fn replace_gap_at(
144        &mut self,
145        events: Vec<Event>,
146        gap_identifier: ChunkIdentifier,
147    ) -> Result<Option<Position>, Error> {
148        // As an optimization, we'll remove the empty chunk if it's a gap.
149        //
150        // However, our linked chunk requires that it includes at least one chunk in the
151        // in-memory representation. We could tweak this invariant, but in the
152        // meanwhile, don't remove the gap chunk if it's the only one we know
153        // about.
154        let has_only_one_chunk = {
155            let mut it = self.chunks.chunks();
156
157            // If there's no chunks at all, then we won't be able to find the gap chunk.
158            let _ =
159                it.next().ok_or(Error::InvalidChunkIdentifier { identifier: gap_identifier })?;
160
161            // If there's no next chunk, we can conclude there's only one.
162            it.next().is_none()
163        };
164
165        let next_pos = if events.is_empty() && !has_only_one_chunk {
166            // There are no new events, so there's no need to create a new empty items
167            // chunk; instead, remove the gap.
168            self.chunks.remove_empty_chunk_at(gap_identifier)?
169        } else {
170            // Replace the gap by new events.
171            Some(self.chunks.replace_gap_at(events, gap_identifier)?.first_position())
172        };
173
174        Ok(next_pos)
175    }
176
177    /// Remove some events from the linked chunk.
178    ///
179    /// If a chunk becomes empty, it's going to be removed.
180    pub fn remove_events_by_position(&mut self, mut positions: Vec<Position>) -> Result<(), Error> {
181        sort_positions_descending(&mut positions);
182
183        for position in positions {
184            self.chunks.remove_item_at(position)?;
185        }
186
187        Ok(())
188    }
189
190    /// Replace event at a specified position.
191    ///
192    /// `position` must point to a valid item, otherwise the method returns an
193    /// error.
194    pub fn replace_event_at(&mut self, position: Position, event: Event) -> Result<(), Error> {
195        self.chunks.replace_item_at(position, event)
196    }
197
198    /// Search for a chunk, and return its identifier.
199    pub fn chunk_identifier<'a, P>(&'a self, predicate: P) -> Option<ChunkIdentifier>
200    where
201        P: FnMut(&'a Chunk<DEFAULT_CHUNK_CAPACITY, Event, Gap>) -> bool,
202    {
203        self.chunks.chunk_identifier(predicate)
204    }
205
206    /// Iterate over the chunks, forward.
207    ///
208    /// The oldest chunk comes first.
209    pub fn chunks(&self) -> Iter<'_, DEFAULT_CHUNK_CAPACITY, Event, Gap> {
210        self.chunks.chunks()
211    }
212
213    /// Iterate over the chunks, backward.
214    ///
215    /// The most recent chunk comes first.
216    pub fn rchunks(&self) -> IterBackward<'_, DEFAULT_CHUNK_CAPACITY, Event, Gap> {
217        self.chunks.rchunks()
218    }
219
220    /// Iterate over the events, backward.
221    ///
222    /// The most recent event comes first.
223    pub fn revents(&self) -> impl Iterator<Item = (Position, &Event)> {
224        self.chunks.ritems()
225    }
226
227    /// Iterate over the events, forward.
228    ///
229    /// The oldest event comes first.
230    pub fn events(&self) -> impl Iterator<Item = (Position, &Event)> {
231        self.chunks.items()
232    }
233
234    /// Get all updates from the room events as [`VectorDiff`].
235    ///
236    /// Be careful that each `VectorDiff` is returned only once!
237    ///
238    /// See [`AsVector`] to learn more.
239    ///
240    /// [`Update`]: matrix_sdk_base::linked_chunk::Update
241    pub fn updates_as_vector_diffs(&mut self) -> Vec<VectorDiff<Event>> {
242        self.chunks_updates_as_vectordiffs.take()
243    }
244
245    /// Get a mutable reference to the [`LinkedChunk`] updates, aka
246    /// [`ObservableUpdates`] to be consumed by the store.
247    ///
248    /// These updates are expected to be *only* forwarded to storage, as they
249    /// might hide some underlying updates to the in-memory chunk; those
250    /// updates should be reflected with manual updates to
251    /// [`Self::chunks_updates_as_vectordiffs`].
252    pub(super) fn store_updates(&mut self) -> &mut ObservableUpdates<Event, Gap> {
253        self.chunks.updates().expect("this is always built with an update history in the ctor")
254    }
255
256    /// Return a nice debug string (a vector of lines) for the linked chunk of
257    /// events for this room.
258    pub fn debug_string(&self) -> Vec<String> {
259        let mut result = Vec::new();
260
261        for chunk in self.chunks() {
262            let content = chunk_debug_string(chunk.content());
263            let lazy_previous = if let Some(cid) = chunk.lazy_previous() {
264                format!(" (lazy previous = {})", cid.index())
265            } else {
266                "".to_owned()
267            };
268            let line = format!("chunk #{}{lazy_previous}: {content}", chunk.identifier().index());
269
270            result.push(line);
271        }
272
273        result
274    }
275
276    /// Return the latest gap, if any.
277    ///
278    /// Latest means "closest to the end", or, since events are ordered
279    /// according to the sync ordering, this means "the most recent one".
280    pub fn rgap(&self) -> Option<Gap> {
281        self.rchunks()
282            .find_map(|chunk| as_variant!(chunk.content(), ChunkContent::Gap(gap) => gap.clone()))
283    }
284}
285
286// Private implementations, implementation specific.
287impl RoomEvents {
288    pub(super) fn insert_new_chunk_as_first(
289        &mut self,
290        raw_new_first_chunk: RawChunk<Event, Gap>,
291    ) -> Result<(), LazyLoaderError> {
292        lazy_loader::insert_new_first_chunk(&mut self.chunks, raw_new_first_chunk)
293    }
294}
295
296/// Create a debug string for a [`ChunkContent`] for an event/gap pair.
297fn chunk_debug_string(content: &ChunkContent<Event, Gap>) -> String {
298    match content {
299        ChunkContent::Gap(Gap { prev_token }) => {
300            format!("gap['{prev_token}']")
301        }
302        ChunkContent::Items(vec) => {
303            let items = vec
304                .iter()
305                .map(|event| {
306                    // Limit event ids to 8 chars *after* the $.
307                    event.event_id().map_or_else(
308                        || "<no event id>".to_owned(),
309                        |id| id.as_str().chars().take(1 + 8).collect(),
310                    )
311                })
312                .collect::<Vec<_>>()
313                .join(", ");
314
315            format!("events[{items}]")
316        }
317    }
318}
319
320/// Sort positions of events so that events can be removed safely without
321/// messing their position.
322///
323/// Events must be sorted by their position index, from greatest to lowest, so
324/// that all positions remain valid inside the same chunk while they are being
325/// removed. For the sake of debugability, we also sort by position chunk
326/// identifier, but this is not required.
327pub(super) fn sort_positions_descending(positions: &mut [Position]) {
328    positions.sort_by(|a, b| {
329        b.chunk_identifier()
330            .cmp(&a.chunk_identifier())
331            .then_with(|| a.index().cmp(&b.index()).reverse())
332    });
333}
334
335#[cfg(test)]
336mod tests {
337    use assert_matches::assert_matches;
338    use assert_matches2::assert_let;
339    use matrix_sdk_test::{event_factory::EventFactory, ALICE, DEFAULT_TEST_ROOM_ID};
340    use ruma::{event_id, user_id, EventId, OwnedEventId};
341
342    use super::*;
343
344    macro_rules! assert_events_eq {
345        ( $events_iterator:expr, [ $( ( $event_id:ident at ( $chunk_identifier:literal, $index:literal ) ) ),* $(,)? ] ) => {
346            {
347                let mut events = $events_iterator;
348
349                $(
350                    assert_let!(Some((position, event)) = events.next());
351                    assert_eq!(position.chunk_identifier(), $chunk_identifier );
352                    assert_eq!(position.index(), $index );
353                    assert_eq!(event.event_id().unwrap(), $event_id );
354                )*
355
356                assert!(events.next().is_none(), "No more events are expected");
357            }
358        };
359    }
360
361    fn new_event(event_id: &str) -> (OwnedEventId, Event) {
362        let event_id = EventId::parse(event_id).unwrap();
363        let event = EventFactory::new()
364            .text_msg("")
365            .sender(user_id!("@mnt_io:matrix.org"))
366            .event_id(&event_id)
367            .into_event();
368
369        (event_id, event)
370    }
371
372    #[test]
373    fn test_new_room_events_has_zero_events() {
374        let room_events = RoomEvents::new();
375
376        assert_eq!(room_events.events().count(), 0);
377    }
378
379    #[test]
380    fn test_push_events() {
381        let (event_id_0, event_0) = new_event("$ev0");
382        let (event_id_1, event_1) = new_event("$ev1");
383        let (event_id_2, event_2) = new_event("$ev2");
384
385        let mut room_events = RoomEvents::new();
386
387        room_events.push_events([event_0, event_1]);
388        room_events.push_events([event_2]);
389
390        assert_events_eq!(
391            room_events.events(),
392            [
393                (event_id_0 at (0, 0)),
394                (event_id_1 at (0, 1)),
395                (event_id_2 at (0, 2)),
396            ]
397        );
398    }
399
400    #[test]
401    fn test_push_gap() {
402        let (event_id_0, event_0) = new_event("$ev0");
403        let (event_id_1, event_1) = new_event("$ev1");
404
405        let mut room_events = RoomEvents::new();
406
407        room_events.push_events([event_0]);
408        room_events.push_gap(Gap { prev_token: "hello".to_owned() });
409        room_events.push_events([event_1]);
410
411        assert_events_eq!(
412            room_events.events(),
413            [
414                (event_id_0 at (0, 0)),
415                (event_id_1 at (2, 0)),
416            ]
417        );
418
419        {
420            let mut chunks = room_events.chunks();
421
422            assert_let!(Some(chunk) = chunks.next());
423            assert!(chunk.is_items());
424
425            assert_let!(Some(chunk) = chunks.next());
426            assert!(chunk.is_gap());
427
428            assert_let!(Some(chunk) = chunks.next());
429            assert!(chunk.is_items());
430
431            assert!(chunks.next().is_none());
432        }
433    }
434
435    #[test]
436    fn test_insert_events_at() {
437        let (event_id_0, event_0) = new_event("$ev0");
438        let (event_id_1, event_1) = new_event("$ev1");
439        let (event_id_2, event_2) = new_event("$ev2");
440
441        let mut room_events = RoomEvents::new();
442
443        room_events.push_events([event_0, event_1]);
444
445        let position_of_event_1 = room_events
446            .events()
447            .find_map(|(position, event)| {
448                (event.event_id().unwrap() == event_id_1).then_some(position)
449            })
450            .unwrap();
451
452        room_events.insert_events_at(vec![event_2], position_of_event_1).unwrap();
453
454        assert_events_eq!(
455            room_events.events(),
456            [
457                (event_id_0 at (0, 0)),
458                (event_id_2 at (0, 1)),
459                (event_id_1 at (0, 2)),
460            ]
461        );
462    }
463
464    #[test]
465    fn test_insert_gap_at() {
466        let (event_id_0, event_0) = new_event("$ev0");
467        let (event_id_1, event_1) = new_event("$ev1");
468
469        let mut room_events = RoomEvents::new();
470
471        room_events.push_events([event_0, event_1]);
472
473        let position_of_event_1 = room_events
474            .events()
475            .find_map(|(position, event)| {
476                (event.event_id().unwrap() == event_id_1).then_some(position)
477            })
478            .unwrap();
479
480        room_events
481            .insert_gap_at(Gap { prev_token: "hello".to_owned() }, position_of_event_1)
482            .unwrap();
483
484        assert_events_eq!(
485            room_events.events(),
486            [
487                (event_id_0 at (0, 0)),
488                (event_id_1 at (2, 0)),
489            ]
490        );
491
492        {
493            let mut chunks = room_events.chunks();
494
495            assert_let!(Some(chunk) = chunks.next());
496            assert!(chunk.is_items());
497
498            assert_let!(Some(chunk) = chunks.next());
499            assert!(chunk.is_gap());
500
501            assert_let!(Some(chunk) = chunks.next());
502            assert!(chunk.is_items());
503
504            assert!(chunks.next().is_none());
505        }
506    }
507
508    #[test]
509    fn test_replace_gap_at() {
510        let (event_id_0, event_0) = new_event("$ev0");
511        let (event_id_1, event_1) = new_event("$ev1");
512        let (event_id_2, event_2) = new_event("$ev2");
513
514        let mut room_events = RoomEvents::new();
515
516        room_events.push_events([event_0]);
517        room_events.push_gap(Gap { prev_token: "hello".to_owned() });
518
519        let chunk_identifier_of_gap = room_events
520            .chunks()
521            .find_map(|chunk| chunk.is_gap().then_some(chunk.identifier()))
522            .unwrap();
523
524        room_events.replace_gap_at(vec![event_1, event_2], chunk_identifier_of_gap).unwrap();
525
526        assert_events_eq!(
527            room_events.events(),
528            [
529                (event_id_0 at (0, 0)),
530                (event_id_1 at (2, 0)),
531                (event_id_2 at (2, 1)),
532            ]
533        );
534
535        {
536            let mut chunks = room_events.chunks();
537
538            assert_let!(Some(chunk) = chunks.next());
539            assert!(chunk.is_items());
540
541            assert_let!(Some(chunk) = chunks.next());
542            assert!(chunk.is_items());
543
544            assert!(chunks.next().is_none());
545        }
546    }
547
548    #[test]
549    fn test_replace_gap_at_with_no_new_events() {
550        let (_, event_0) = new_event("$ev0");
551        let (_, event_1) = new_event("$ev1");
552        let (_, event_2) = new_event("$ev2");
553
554        let mut room_events = RoomEvents::new();
555
556        room_events.push_events([event_0, event_1]);
557        room_events.push_gap(Gap { prev_token: "middle".to_owned() });
558        room_events.push_events([event_2]);
559        room_events.push_gap(Gap { prev_token: "end".to_owned() });
560
561        // Remove the first gap.
562        let first_gap_id = room_events
563            .chunks()
564            .find_map(|chunk| chunk.is_gap().then_some(chunk.identifier()))
565            .unwrap();
566
567        // The next insert position is the next chunk's start.
568        let pos = room_events.replace_gap_at(vec![], first_gap_id).unwrap();
569        assert_eq!(pos, Some(Position::new(ChunkIdentifier::new(2), 0)));
570
571        // Remove the second gap.
572        let second_gap_id = room_events
573            .chunks()
574            .find_map(|chunk| chunk.is_gap().then_some(chunk.identifier()))
575            .unwrap();
576
577        // No next insert position.
578        let pos = room_events.replace_gap_at(vec![], second_gap_id).unwrap();
579        assert!(pos.is_none());
580    }
581
582    #[test]
583    fn test_remove_events() {
584        let (event_id_0, event_0) = new_event("$ev0");
585        let (event_id_1, event_1) = new_event("$ev1");
586        let (event_id_2, event_2) = new_event("$ev2");
587        let (event_id_3, event_3) = new_event("$ev3");
588
589        // Push some events.
590        let mut room_events = RoomEvents::new();
591        room_events.push_events([event_0, event_1]);
592        room_events.push_gap(Gap { prev_token: "hello".to_owned() });
593        room_events.push_events([event_2, event_3]);
594
595        assert_events_eq!(
596            room_events.events(),
597            [
598                (event_id_0 at (0, 0)),
599                (event_id_1 at (0, 1)),
600                (event_id_2 at (2, 0)),
601                (event_id_3 at (2, 1)),
602            ]
603        );
604        assert_eq!(room_events.chunks().count(), 3);
605
606        // Remove some events.
607        room_events
608            .remove_events_by_position(vec![
609                Position::new(ChunkIdentifier::new(2), 1),
610                Position::new(ChunkIdentifier::new(0), 1),
611            ])
612            .unwrap();
613
614        assert_events_eq!(
615            room_events.events(),
616            [
617                (event_id_0 at (0, 0)),
618                (event_id_2 at (2, 0)),
619            ]
620        );
621
622        // Ensure chunks are removed once empty.
623        room_events
624            .remove_events_by_position(vec![Position::new(ChunkIdentifier::new(2), 0)])
625            .unwrap();
626
627        assert_events_eq!(
628            room_events.events(),
629            [
630                (event_id_0 at (0, 0)),
631            ]
632        );
633        assert_eq!(room_events.chunks().count(), 2);
634    }
635
636    #[test]
637    fn test_remove_events_unknown_event() {
638        // Push ZERO event.
639        let mut room_events = RoomEvents::new();
640
641        assert_events_eq!(room_events.events(), []);
642
643        // Remove one undefined event.
644        // An error is expected.
645        room_events
646            .remove_events_by_position(vec![Position::new(ChunkIdentifier::new(42), 153)])
647            .unwrap_err();
648
649        assert_events_eq!(room_events.events(), []);
650
651        let mut events = room_events.events();
652        assert!(events.next().is_none());
653    }
654
655    #[test]
656    fn test_reset() {
657        let (event_id_0, event_0) = new_event("$ev0");
658        let (event_id_1, event_1) = new_event("$ev1");
659        let (event_id_2, event_2) = new_event("$ev2");
660        let (event_id_3, event_3) = new_event("$ev3");
661
662        // Push some events.
663        let mut room_events = RoomEvents::new();
664        room_events.push_events([event_0, event_1]);
665        room_events.push_gap(Gap { prev_token: "raclette".to_owned() });
666        room_events.push_events([event_2]);
667
668        // Read the updates as `VectorDiff`.
669        let diffs = room_events.updates_as_vector_diffs();
670
671        assert_eq!(diffs.len(), 2);
672
673        assert_matches!(
674            &diffs[0],
675            VectorDiff::Append { values } => {
676                assert_eq!(values.len(), 2);
677                assert_eq!(values[0].event_id(), Some(event_id_0));
678                assert_eq!(values[1].event_id(), Some(event_id_1));
679            }
680        );
681        assert_matches!(
682            &diffs[1],
683            VectorDiff::Append { values } => {
684                assert_eq!(values.len(), 1);
685                assert_eq!(values[0].event_id(), Some(event_id_2));
686            }
687        );
688
689        // Now we can reset and see what happens.
690        room_events.reset();
691        room_events.push_events([event_3]);
692
693        // Read the updates as `VectorDiff`.
694        let diffs = room_events.updates_as_vector_diffs();
695
696        assert_eq!(diffs.len(), 2);
697
698        assert_matches!(&diffs[0], VectorDiff::Clear);
699        assert_matches!(
700            &diffs[1],
701            VectorDiff::Append { values } => {
702                assert_eq!(values.len(), 1);
703                assert_eq!(values[0].event_id(), Some(event_id_3));
704            }
705        );
706    }
707
708    #[test]
709    fn test_debug_string() {
710        let event_factory = EventFactory::new().room(&DEFAULT_TEST_ROOM_ID).sender(*ALICE);
711
712        let mut room_events = RoomEvents::new();
713        room_events.push_events(vec![
714            event_factory
715                .text_msg("hey")
716                .event_id(event_id!("$123456789101112131415617181920"))
717                .into_event(),
718            event_factory.text_msg("you").event_id(event_id!("$2")).into_event(),
719        ]);
720        room_events.push_gap(Gap { prev_token: "raclette".to_owned() });
721
722        let output = room_events.debug_string();
723
724        assert_eq!(output.len(), 2);
725        assert_eq!(&output[0], "chunk #0: events[$12345678, $2]");
726        assert_eq!(&output[1], "chunk #1: gap['raclette']");
727    }
728
729    #[test]
730    fn test_sort_positions_descending() {
731        let mut positions = vec![
732            Position::new(ChunkIdentifier::new(2), 1),
733            Position::new(ChunkIdentifier::new(1), 0),
734            Position::new(ChunkIdentifier::new(2), 0),
735            Position::new(ChunkIdentifier::new(1), 1),
736            Position::new(ChunkIdentifier::new(0), 0),
737        ];
738
739        sort_positions_descending(&mut positions);
740
741        assert_eq!(
742            positions,
743            &[
744                Position::new(ChunkIdentifier::new(2), 1),
745                Position::new(ChunkIdentifier::new(2), 0),
746                Position::new(ChunkIdentifier::new(1), 1),
747                Position::new(ChunkIdentifier::new(1), 0),
748                Position::new(ChunkIdentifier::new(0), 0),
749            ]
750        );
751    }
752}