atagen

macltok

public
Star0
Watch0
0e081dee5e28debddd20b865cc576862c03a2a66
3
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git
tests/history_repair.rsRust616 lines20.0 KB
1
mod support;
2
3
use std::time::Duration;
4
5
#[cfg(feature = "mirror")]
6
use macltok::Mirror;
7
use macltok::{
8
    ClientHandle, Engine, EngineConfig, EngineError, Event, EventStream, HistoryGap,
9
    HistoryRepairPhase, HistoryRepairReport, HistoryRepairStop, RecentHistoryState, Termination,
10
    TimelineDiff, TimelineId, TimelineItem,
11
};
12
use matrix_sdk::{
13
    Client,
14
    ruma::{EventId, RoomId, api::client::sync::sync_events::v5, event_id, room_id},
15
    test_utils::mocks::{MatrixMockServer, RoomMessagesResponseTemplate},
16
};
17
use matrix_sdk_test::{ALICE, JoinedRoomBuilder, event_factory::EventFactory};
18
use support::shut_down;
19
use wiremock::ResponseTemplate;
20
21
struct TestRoom {
22
    server: MatrixMockServer,
23
    sync_client: Client,
24
    handle: ClientHandle,
25
    events: EventStream,
26
    running: tokio::task::JoinHandle<Result<Termination, EngineError>>,
27
    /// the open room's timeline, kept current by every helper that reads events
28
    items: imbl::Vector<TimelineItem>,
29
    opened: bool,
30
}
31
32
async fn start_room(room_id: &RoomId) -> TestRoom {
33
    let server = MatrixMockServer::new().await;
34
    server
35
        .mock_sliding_sync()
36
        .ok(v5::Response::new("1".to_owned()))
37
        .mount()
38
        .await;
39
    let client = server.client_builder().build().await;
40
    server.mock_room_state_encryption().plain().mount().await;
41
    server.sync_joined_room(&client, room_id).await;
42
    let sync_client = client.clone();
43
    let config = EngineConfig::default()
44
        .with_initial_history_limit(0)
45
        .with_search_backfill_pages_per_room(0);
46
    let (engine, handle, events) = Engine::from_client(client, config).await.unwrap();
47
    let running = tokio::spawn(engine.run());
48
    TestRoom {
49
        server,
50
        sync_client,
51
        handle,
52
        events,
53
        running,
54
        items: imbl::Vector::new(),
55
        opened: false,
56
    }
57
}
58
59
/// the next event that satisfies `predicate`. only the open itself may reset the room
60
/// timeline, history after it has to arrive as diffs
61
async fn next_until(room: &mut TestRoom, mut predicate: impl FnMut(&Event) -> bool) -> Event {
62
    tokio::time::timeout(Duration::from_secs(5), async {
63
        loop {
64
            let event = room.events.recv().await.expect("event stream closed");
65
            match &event {
66
                Event::TimelineReset {
67
                    timeline_id: TimelineId::Room(_),
68
                    items,
69
                } => {
70
                    assert!(!room.opened, "history reset the room timeline");
71
                    room.opened = true;
72
                    room.items = items.clone();
73
                }
74
                Event::TimelineChanged {
75
                    timeline_id: TimelineId::Room(_),
76
                    diffs,
77
                } => {
78
                    for diff in diffs.clone() {
79
                        assert!(
80
                            !matches!(diff, TimelineDiff::Reset { .. }),
81
                            "history replaced the room timeline"
82
                        );
83
                        diff.try_apply(&mut room.items)
84
                            .expect("diff did not fit the timeline");
85
                    }
86
                }
87
                _ => {}
88
            }
89
            if predicate(&event) {
90
                return event;
91
            }
92
        }
93
    })
94
    .await
95
    .expect("expected event did not arrive")
96
}
97
98
async fn timeline_until(
99
    room: &mut TestRoom,
100
    predicate: impl Fn(&imbl::Vector<TimelineItem>) -> bool,
101
) {
102
    tokio::time::timeout(Duration::from_secs(5), async {
103
        while !predicate(&room.items) {
104
            next_until(room, |_| true).await;
105
        }
106
    })
107
    .await
108
    .expect("the timeline never got there");
109
}
110
111
async fn open_room(room: &mut TestRoom, room_id: &RoomId) {
112
    room.handle.open_room(room_id.to_owned()).await.unwrap();
113
    next_until(room, |event| {
114
        matches!(event, Event::TimelineReset { timeline_id, .. } if timeline_id.room_id() == room_id)
115
    })
116
    .await;
117
}
118
119
async fn cache_event_before_open(room: &TestRoom, room_id: &RoomId, id: &EventId, body: &str) {
120
    room.server
121
        .sync_room(
122
            &room.sync_client,
123
            JoinedRoomBuilder::new(room_id).add_timeline_event(
124
                EventFactory::new()
125
                    .text_msg(body)
126
                    .event_id(id)
127
                    .sender(&ALICE),
128
            ),
129
        )
130
        .await;
131
}
132
133
fn history_event(
134
    room_id: &RoomId,
135
    id: &EventId,
136
    body: &str,
137
) -> matrix_sdk::ruma::serde::Raw<matrix_sdk::ruma::events::AnyTimelineEvent> {
138
    EventFactory::new()
139
        .room(room_id)
140
        .text_msg(body)
141
        .event_id(id)
142
        .sender(&ALICE)
143
        .into_raw_timeline()
144
}
145
146
async fn wait_for_recent(room: &mut TestRoom, room_id: &RoomId) -> RecentHistoryState {
147
    let event = next_until(room, |event| {
148
        matches!(event, Event::RecentHistoryChanged { room_id: id, state, .. }
149
            if id == room_id && !matches!(state, RecentHistoryState::Loading))
150
    })
151
    .await;
152
    match event {
153
        Event::RecentHistoryChanged { state, .. } => state,
154
        _ => unreachable!(),
155
    }
156
}
157
158
async fn wait_for_settled_fill(room: &mut TestRoom) -> HistoryRepairReport {
159
    match next_until(room, |event| {
160
        matches!(event, Event::HistoryRepairChanged(report)
161
            if report.phase != HistoryRepairPhase::Bridging)
162
    })
163
    .await
164
    {
165
        Event::HistoryRepairChanged(report) => report,
166
        _ => unreachable!(),
167
    }
168
}
169
170
fn has_event(items: &imbl::Vector<TimelineItem>, id: &EventId) -> bool {
171
    items.iter().any(
172
        |item| matches!(item, TimelineItem::Event(event) if event.event_id.as_deref() == Some(id)),
173
    )
174
}
175
176
fn gap_in(items: &imbl::Vector<TimelineItem>) -> Option<HistoryGap> {
177
    items.iter().find_map(|item| match item {
178
        TimelineItem::HistoryGap(gap) => Some(gap.clone()),
179
        _ => None,
180
    })
181
}
182
183
#[tokio::test]
184
async fn automatic_recent_load_is_bounded_and_keeps_cached_history_behind_a_gap() {
185
    let room_id = room_id!("!recent-repair:localhost");
186
    let old_id = event_id!("$old:localhost");
187
    let new_ids = [
188
        event_id!("$fresh-a:localhost"),
189
        event_id!("$fresh-b:localhost"),
190
        event_id!("$fresh-c:localhost"),
191
        event_id!("$fresh-d:localhost"),
192
    ];
193
    let mut room = start_room(room_id).await;
194
    for (index, id) in new_ids.iter().enumerate() {
195
        let token = format!("recent-{index}");
196
        let response = RoomMessagesResponseTemplate::default()
197
            .end_token(token.clone())
198
            .events(vec![history_event(room_id, id, "recent event")]);
199
        if index > 0 {
200
            let previous = format!("recent-{}", index - 1);
201
            room.server
202
                .mock_room_messages()
203
                .match_from(&previous)
204
                .ok(response)
205
                .mock_once()
206
                .mount()
207
                .await;
208
        } else {
209
            room.server
210
                .mock_room_messages()
211
                .ok(response)
212
                .mock_once()
213
                .mount()
214
                .await;
215
        }
216
    }
217
    cache_event_before_open(&room, room_id, old_id, "retained old event").await;
218
    open_room(&mut room, room_id).await;
219
    timeline_until(&mut room, |items| {
220
        gap_in(items).is_some()
221
            && has_event(items, old_id)
222
            && new_ids.iter().all(|id| has_event(items, id))
223
    })
224
    .await;
225
    assert_eq!(
226
        wait_for_recent(&mut room, room_id).await,
227
        RecentHistoryState::Loaded
228
    );
229
    let requests = room.server.received_requests().await.unwrap();
230
    let messages = requests
231
        .iter()
232
        .filter(|request| request.url.path().ends_with("/messages"))
233
        .collect::<Vec<_>>();
234
    assert_eq!(messages.len(), 4);
235
    assert!(!messages[0].url.query_pairs().any(|(key, _)| key == "from"));
236
    shut_down(&room.handle, &mut room.events, room.running).await;
237
}
238
239
#[tokio::test]
240
async fn explicit_recent_start_coalesces_with_the_automatic_load() {
241
    let room_id = room_id!("!recent-coalesce:localhost");
242
    let fresh_id = event_id!("$coalesced:localhost");
243
    let mut room = start_room(room_id).await;
244
    room.server
245
        .mock_room_messages()
246
        .ok(RoomMessagesResponseTemplate::default()
247
            .with_delay(Duration::from_millis(300))
248
            .events(vec![history_event(room_id, fresh_id, "shared result")]))
249
        .mock_once()
250
        .mount()
251
        .await;
252
    open_room(&mut room, room_id).await;
253
    tokio::time::timeout(Duration::from_secs(5), async {
254
        loop {
255
            let requested = room
256
                .server
257
                .received_requests()
258
                .await
259
                .unwrap()
260
                .iter()
261
                .any(|request| request.url.path().ends_with("/messages"));
262
            if requested {
263
                break;
264
            }
265
            tokio::task::yield_now().await;
266
        }
267
    })
268
    .await
269
    .expect("automatic recent request did not start");
270
271
    room.handle
272
        .retry_recent_history(room_id.to_owned())
273
        .await
274
        .unwrap();
275
    assert_eq!(
276
        wait_for_recent(&mut room, room_id).await,
277
        RecentHistoryState::Loaded
278
    );
279
    let requests = room.server.received_requests().await.unwrap();
280
    assert_eq!(
281
        requests
282
            .iter()
283
            .filter(|request| request.url.path().ends_with("/messages"))
284
            .count(),
285
        1
286
    );
287
    shut_down(&room.handle, &mut room.events, room.running).await;
288
}
289
290
#[cfg(feature = "mirror")]
291
#[tokio::test]
292
async fn fill_gap_returns_after_the_mirror_contains_the_committed_rows() {
293
    let server = MatrixMockServer::new().await;
294
    server
295
        .mock_sliding_sync()
296
        .ok(v5::Response::new("1".to_owned()))
297
        .mount()
298
        .await;
299
    let client = server.client_builder().build().await;
300
    let room_id = room_id!("!fill-gap-mirror:localhost");
301
    server.mock_room_state_encryption().plain().mount().await;
302
    server.sync_joined_room(&client, room_id).await;
303
    let sync_client = client.clone();
304
    let old_id = event_id!("$gap-old:localhost");
305
    let fresh_id = event_id!("$gap-fresh:localhost");
306
    let middle_id = event_id!("$gap-middle:localhost");
307
    let (engine, handle, events) = Engine::from_client(
308
        client,
309
        EngineConfig::default()
310
            .with_initial_history_limit(0)
311
            .with_search_backfill_pages_per_room(0),
312
    )
313
    .await
314
    .unwrap();
315
    let running = tokio::spawn(engine.run());
316
    let (mirror, mirror_handle) = Mirror::new(handle.clone(), events);
317
    let mirror_running = tokio::spawn(mirror.run());
318
    server
319
        .mock_room_messages()
320
        .ok(RoomMessagesResponseTemplate::default()
321
            .end_token("recent-gap")
322
            .events(vec![history_event(room_id, fresh_id, "new window")]))
323
        .mock_once()
324
        .mount()
325
        .await;
326
    server
327
        .mock_room_messages()
328
        .match_from("recent-gap")
329
        .ok(RoomMessagesResponseTemplate::default().end_token("recent-gap"))
330
        .mock_once()
331
        .mount()
332
        .await;
333
    server
334
        .sync_room(
335
            &sync_client,
336
            JoinedRoomBuilder::new(room_id).add_timeline_event(
337
                EventFactory::new()
338
                    .text_msg("old retained")
339
                    .event_id(old_id)
340
                    .sender(&ALICE),
341
            ),
342
        )
343
        .await;
344
    handle.open_room(room_id.to_owned()).await.unwrap();
345
    let timeline_id = TimelineId::from(room_id.to_owned());
346
    let gap = tokio::time::timeout(Duration::from_secs(5), async {
347
        loop {
348
            if let Some(gap) = mirror_handle
349
                .snapshot()
350
                .timelines
351
                .get(&timeline_id)
352
                .and_then(|timeline| gap_in(&timeline.items))
353
            {
354
                break gap;
355
            }
356
            tokio::task::yield_now().await;
357
        }
358
    })
359
    .await
360
    .expect("the recent history gap did not reach the mirror");
361
    tokio::time::timeout(Duration::from_secs(5), async {
362
        loop {
363
            if mirror_handle.snapshot().recent_history.get(room_id)
364
                == Some(&RecentHistoryState::Loaded)
365
            {
366
                break;
367
            }
368
            tokio::task::yield_now().await;
369
        }
370
    })
371
    .await
372
    .expect("automatic recent loading did not finish");
373
    server
374
        .mock_room_messages()
375
        .match_from("recent-gap")
376
        .ok(RoomMessagesResponseTemplate::default()
377
            .with_delay(Duration::from_millis(300))
378
            .events(vec![
379
                history_event(room_id, middle_id, "middle page"),
380
                history_event(room_id, old_id, "old retained overlap"),
381
            ]))
382
        .mock_once()
383
        .mount()
384
        .await;
385
386
    let fill_handle = handle.clone();
387
    let target_gap_id = gap.id.clone();
388
    let fill_gap_id = target_gap_id.clone();
389
    let fill = tokio::spawn(async move {
390
        fill_handle
391
            .fill_history_gap(room_id.to_owned(), fill_gap_id)
392
            .await
393
    });
394
    tokio::time::timeout(Duration::from_secs(5), async {
395
        loop {
396
            let snapshot = mirror_handle.snapshot();
397
            let active_gap = snapshot.repairs.get(room_id).is_some_and(|report| {
398
                report.gap_id == target_gap_id && report.phase == HistoryRepairPhase::Bridging
399
            });
400
            if active_gap {
401
                break;
402
            }
403
            tokio::task::yield_now().await;
404
        }
405
    })
406
    .await
407
    .expect("the active repair report did not identify its target gap");
408
    fill.await.unwrap().unwrap();
409
    let snapshot = mirror_handle.snapshot();
410
    let timeline = snapshot
411
        .timelines
412
        .get(&timeline_id)
413
        .expect("the room timeline disappeared");
414
    let event_ids = timeline
415
        .items
416
        .iter()
417
        .filter_map(|item| match item {
418
            TimelineItem::Event(event) => event.event_id.as_deref(),
419
            _ => None,
420
        })
421
        .collect::<Vec<_>>();
422
    assert!(event_ids.contains(&fresh_id));
423
    assert!(event_ids.contains(&middle_id));
424
    assert!(event_ids.contains(&old_id));
425
426
    handle.shutdown().await.unwrap();
427
    assert_eq!(running.await.unwrap().unwrap(), Termination::ShutDown);
428
    mirror_running.await.unwrap();
429
}
430
431
#[tokio::test]
432
async fn cancelling_a_gap_fill_keeps_committed_progress_for_retry() {
433
    let room_id = room_id!("!cancel-gap-fill:localhost");
434
    let old_id = event_id!("$cancel-gap-old:localhost");
435
    let fresh_id = event_id!("$cancel-gap-fresh:localhost");
436
    let middle_id = event_id!("$cancel-gap-middle:localhost");
437
    let mut room = start_room(room_id).await;
438
    room.server
439
        .mock_room_messages()
440
        .ok(RoomMessagesResponseTemplate::default()
441
            .end_token("gap-cursor-1")
442
            .events(vec![history_event(room_id, fresh_id, "recent")]))
443
        .mock_once()
444
        .mount()
445
        .await;
446
    room.server
447
        .mock_room_messages()
448
        .match_from("gap-cursor-1")
449
        .ok(RoomMessagesResponseTemplate::default().end_token("gap-cursor-1"))
450
        .mock_once()
451
        .mount()
452
        .await;
453
    cache_event_before_open(&room, room_id, old_id, "old cached").await;
454
    open_room(&mut room, room_id).await;
455
    timeline_until(&mut room, |items| gap_in(items).is_some()).await;
456
    let gap = gap_in(&room.items).unwrap();
457
    assert_eq!(
458
        wait_for_recent(&mut room, room_id).await,
459
        RecentHistoryState::Loaded
460
    );
461
    room.server
462
        .mock_room_messages()
463
        .match_from("gap-cursor-1")
464
        .ok(RoomMessagesResponseTemplate::default()
465
            .end_token("gap-cursor-2")
466
            .events(vec![history_event(
467
                room_id,
468
                middle_id,
469
                "committed progress",
470
            )]))
471
        .mock_once()
472
        .mount()
473
        .await;
474
    room.server
475
        .mock_room_messages()
476
        .match_from("gap-cursor-2")
477
        .ok(RoomMessagesResponseTemplate::default()
478
            .with_delay(Duration::from_secs(5))
479
            .events(vec![history_event(room_id, old_id, "old overlap")]))
480
        .mock_once()
481
        .mount()
482
        .await;
483
    let fill_handle = room.handle.clone();
484
    let gap_id = gap.id.clone();
485
    let fill = tokio::spawn(async move {
486
        fill_handle
487
            .fill_history_gap(room_id.to_owned(), gap_id)
488
            .await
489
    });
490
    tokio::time::timeout(Duration::from_secs(5), async {
491
        loop {
492
            let requested = room
493
                .server
494
                .received_requests()
495
                .await
496
                .unwrap()
497
                .iter()
498
                .any(|request| {
499
                    request.url.path().ends_with("/messages")
500
                        && request
501
                            .url
502
                            .query_pairs()
503
                            .any(|(key, value)| key == "from" && value == "gap-cursor-2")
504
                });
505
            if requested {
506
                break;
507
            }
508
            tokio::task::yield_now().await;
509
        }
510
    })
511
    .await
512
    .expect("the second gap page did not start");
513
    room.handle
514
        .cancel_history_repair(room_id.to_owned())
515
        .await
516
        .unwrap();
517
    let error = fill.await.unwrap().unwrap_err();
518
    assert_eq!(error.kind, macltok::ErrorKind::Interrupted);
519
    let report = wait_for_settled_fill(&mut room).await;
520
    assert_eq!(report.stop, Some(HistoryRepairStop::Cancelled));
521
    assert!(report.failure.is_none());
522
523
    room.server
524
        .mock_room_messages()
525
        .match_from("gap-cursor-2")
526
        .ok(
527
            RoomMessagesResponseTemplate::default().events(vec![history_event(
528
                room_id,
529
                old_id,
530
                "old overlap",
531
            )]),
532
        )
533
        .mock_once()
534
        .mount()
535
        .await;
536
    room.handle
537
        .fill_history_gap(room_id.to_owned(), gap.id)
538
        .await
539
        .unwrap();
540
    timeline_until(&mut room, |items| {
541
        [fresh_id, middle_id, old_id]
542
            .iter()
543
            .all(|id| has_event(items, id))
544
            && gap_in(items).is_none()
545
    })
546
    .await;
547
    shut_down(&room.handle, &mut room.events, room.running).await;
548
}
549
550
#[tokio::test]
551
async fn failed_automatic_recent_load_is_public_and_keeps_the_cached_timeline() {
552
    let room_id = room_id!("!failed-recent:localhost");
553
    let old_id = event_id!("$failed-old:localhost");
554
    let mut room = start_room(room_id).await;
555
    room.server
556
        .mock_room_messages()
557
        .respond_with(ResponseTemplate::new(403).set_body_json(serde_json::json!({
558
            "errcode": "M_FORBIDDEN",
559
            "error": "history is unavailable",
560
        })))
561
        .mock_once()
562
        .mount()
563
        .await;
564
    cache_event_before_open(&room, room_id, old_id, "kept event").await;
565
    open_room(&mut room, room_id).await;
566
    let state = wait_for_recent(&mut room, room_id).await;
567
    assert!(matches!(state, RecentHistoryState::Failed(_)));
568
    shut_down(&room.handle, &mut room.events, room.running).await;
569
}
570
571
#[tokio::test]
572
async fn closing_during_a_recent_load_settles_idle_not_failed() {
573
    let room_id = room_id!("!close-recent:localhost");
574
    let mut room = start_room(room_id).await;
575
    room.server
576
        .mock_room_messages()
577
        .ok(RoomMessagesResponseTemplate::default()
578
            .with_delay(Duration::from_secs(5))
579
            .events(vec![history_event(
580
                room_id,
581
                event_id!("$never:localhost"),
582
                "too late",
583
            )]))
584
        .mock_once()
585
        .mount()
586
        .await;
587
    open_room(&mut room, room_id).await;
588
    tokio::time::timeout(Duration::from_secs(5), async {
589
        while !room
590
            .server
591
            .received_requests()
592
            .await
593
            .unwrap()
594
            .iter()
595
            .any(|request| request.url.path().ends_with("/messages"))
596
        {
597
            tokio::task::yield_now().await;
598
        }
599
    })
600
    .await
601
    .expect("the recent load never asked for messages");
602
    room.handle.close_room(room_id.to_owned()).await.unwrap();
603
    let mut states = Vec::new();
604
    next_until(&mut room, |event| {
605
        if let Event::RecentHistoryChanged { state, .. } = event {
606
            states.push(state.clone());
607
        }
608
        matches!(event, Event::TimelineClosed { timeline_id } if timeline_id.room_id() == room_id)
609
    })
610
    .await;
611
    assert_eq!(
612
        states,
613
        [RecentHistoryState::Loading, RecentHistoryState::Idle]
614
    );
615
    shut_down(&room.handle, &mut room.events, room.running).await;
616
}