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