atagen

macltok

public
Star0
Watch0
66db1b3cd8955da257726f2f3028b931ba34c3c0
3
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git
tests/mirror.rsRust530 lines16.1 KB
1
#![cfg(feature = "mirror")]
2
3
use std::time::Duration;
4
5
use futures_util::StreamExt;
6
use macltok::{
7
    ClientHandle, Command, ConnectionState, Engine, EngineConfig, Event, MentionCoverage,
8
    MentionsBoundary, MentionsPage, MentionsUpdate, Mirror, MirrorHandle, StateChange,
9
    Subscription, TimelineChange, TimelineCommand, TimelineDiff, TimelineId, TimelineItem, Update,
10
    UploadOutcome,
11
};
12
use matrix_sdk::{
13
    ruma::{
14
        OwnedRoomId, OwnedTransactionId, TransactionId, api::client::sync::sync_events::v5,
15
        event_id, room_id, user_id,
16
    },
17
    test_utils::mocks::MatrixMockServer,
18
};
19
use tokio::sync::mpsc;
20
21
struct Harness {
22
    handle: ClientHandle,
23
    mirror: MirrorHandle,
24
    inject: mpsc::UnboundedSender<Event>,
25
    room_id: OwnedRoomId,
26
    _server: MatrixMockServer,
27
}
28
29
/// the mirror reads the real engine's stream with synthetic events mixed in, so a test can
30
/// hand it a diff the engine never would
31
async fn start(config: EngineConfig, capacity: usize) -> Harness {
32
    let server = MatrixMockServer::new().await;
33
    server
34
        .mock_sliding_sync()
35
        .ok(v5::Response::new("1".to_owned()))
36
        .mount()
37
        .await;
38
    let client = server.client_builder().build().await;
39
    let room_id = room_id!("!mirror:localhost");
40
    server.mock_room_state_encryption().plain().mount().await;
41
    server.sync_joined_room(&client, room_id).await;
42
    let (engine, handle, events) =
43
        Engine::from_client(client, config.with_initial_history_limit(0))
44
            .await
45
            .unwrap();
46
    tokio::spawn(engine.run());
47
    let (inject, mut injected) = mpsc::unbounded_channel();
48
    let injected = futures_util::stream::poll_fn(move |context| injected.poll_recv(context));
49
    let (mirror, mirror_handle) = Mirror::with_capacity(
50
        handle.clone(),
51
        futures_util::stream::select(events, injected),
52
        capacity,
53
    );
54
    tokio::spawn(mirror.run());
55
    Harness {
56
        handle,
57
        mirror: mirror_handle,
58
        inject,
59
        room_id: room_id.to_owned(),
60
        _server: server,
61
    }
62
}
63
64
async fn next_matching(
65
    subscription: &mut Subscription,
66
    predicate: impl Fn(&StateChange) -> bool,
67
) -> Update {
68
    tokio::time::timeout(Duration::from_secs(5), async {
69
        loop {
70
            let update = subscription.next().await.expect("the mirror stopped");
71
            if predicate(&update.change) {
72
                return update;
73
            }
74
        }
75
    })
76
    .await
77
    .expect("expected update did not arrive")
78
}
79
80
fn timeline_change(
81
    change: &StateChange,
82
    id: &TimelineId,
83
    predicate: impl Fn(&TimelineChange) -> bool,
84
) -> bool {
85
    matches!(change, StateChange::Timeline { timeline_id, change } if timeline_id == id && predicate(change))
86
}
87
88
fn item(timestamp_ms: u64) -> TimelineItem {
89
    TimelineItem::DayDivider { timestamp_ms }
90
}
91
92
fn reset(timeline_id: &TimelineId, items: Vec<TimelineItem>) -> Event {
93
    Event::TimelineReset {
94
        timeline_id: timeline_id.clone(),
95
        items: items.into(),
96
    }
97
}
98
99
/// the room never opens, so every resync is refused and the divergence holds still to be looked at
100
#[tokio::test]
101
async fn diverged_batch_keeps_last_good_items_until_a_reset() {
102
    let harness = start(EngineConfig::default(), 256).await;
103
    let timeline_id = TimelineId::from(harness.room_id.clone());
104
    let mut updates = harness.mirror.subscribe();
105
    harness
106
        .inject
107
        .send(reset(&timeline_id, vec![item(1), item(2)]))
108
        .unwrap();
109
    for diffs in [
110
        vec![
111
            TimelineDiff::PushBack { value: item(3) },
112
            TimelineDiff::Remove { index: 7 },
113
        ],
114
        vec![TimelineDiff::PushBack { value: item(4) }],
115
    ] {
116
        harness
117
            .inject
118
            .send(Event::TimelineChanged {
119
                timeline_id: timeline_id.clone(),
120
                diffs,
121
            })
122
            .unwrap();
123
    }
124
    next_matching(&mut updates, |change| {
125
        timeline_change(change, &timeline_id, |change| {
126
            matches!(change, TimelineChange::Diverged)
127
        })
128
    })
129
    .await;
130
    let snapshot = harness.mirror.snapshot();
131
    let timeline = &snapshot.timelines[&timeline_id];
132
    assert!(timeline.diverged);
133
    assert_eq!(timeline.items, [item(1), item(2)].into_iter().collect());
134
135
    harness
136
        .inject
137
        .send(reset(&timeline_id, vec![item(5)]))
138
        .unwrap();
139
    let update = next_matching(&mut updates, |change| {
140
        timeline_change(change, &timeline_id, |_| true)
141
    })
142
    .await;
143
    assert!(
144
        timeline_change(&update.change, &timeline_id, |change| {
145
            matches!(change, TimelineChange::Reset { .. })
146
        }),
147
        "a diverged timeline took {update:?}"
148
    );
149
    let snapshot = harness.mirror.snapshot();
150
    let timeline = &snapshot.timelines[&timeline_id];
151
    assert!(!timeline.diverged);
152
    assert_eq!(timeline.items, [item(5)].into_iter().collect());
153
}
154
155
#[tokio::test]
156
async fn a_diff_for_a_timeline_without_a_reset_diverges_it() {
157
    let harness = start(EngineConfig::default(), 256).await;
158
    let timeline_id = TimelineId::from(harness.room_id.clone());
159
    let mut updates = harness.mirror.subscribe();
160
    harness
161
        .inject
162
        .send(Event::TimelineChanged {
163
            timeline_id: timeline_id.clone(),
164
            diffs: vec![TimelineDiff::PushBack { value: item(1) }],
165
        })
166
        .unwrap();
167
    next_matching(&mut updates, |change| {
168
        timeline_change(change, &timeline_id, |change| {
169
            matches!(change, TimelineChange::Diverged)
170
        })
171
    })
172
    .await;
173
    let snapshot = harness.mirror.snapshot();
174
    let timeline = &snapshot.timelines[&timeline_id];
175
    assert!(timeline.diverged);
176
    assert!(timeline.items.is_empty());
177
}
178
179
#[tokio::test]
180
async fn room_and_thread_timelines_stay_apart() {
181
    let harness = start(EngineConfig::default(), 256).await;
182
    let room = TimelineId::from(harness.room_id.clone());
183
    let thread = TimelineId::thread(
184
        harness.room_id.clone(),
185
        event_id!("$root:localhost").to_owned(),
186
    );
187
    let mut updates = harness.mirror.subscribe();
188
    harness.inject.send(reset(&room, vec![item(1)])).unwrap();
189
    harness.inject.send(reset(&thread, vec![item(2)])).unwrap();
190
    harness
191
        .inject
192
        .send(Event::TimelineChanged {
193
            timeline_id: thread.clone(),
194
            diffs: vec![TimelineDiff::PushBack { value: item(3) }],
195
        })
196
        .unwrap();
197
    next_matching(&mut updates, |change| {
198
        timeline_change(change, &thread, |change| {
199
            matches!(change, TimelineChange::Changed { .. })
200
        })
201
    })
202
    .await;
203
    let snapshot = harness.mirror.snapshot();
204
    assert_eq!(
205
        snapshot.timelines[&room].items,
206
        [item(1)].into_iter().collect()
207
    );
208
    assert_eq!(
209
        snapshot.timelines[&thread].items,
210
        [item(2), item(3)].into_iter().collect()
211
    );
212
}
213
214
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
215
async fn subscribers_joining_mid_stream_see_no_gap_and_nothing_twice() {
216
    const CHUNKS: u64 = 64;
217
    const CHUNK: u64 = 32;
218
    let harness = start(EngineConfig::default(), 4096).await;
219
    let transaction_id = TransactionId::new();
220
    let mut followers = Vec::new();
221
    for chunk in 0..CHUNKS {
222
        for uploaded_bytes in chunk * CHUNK + 1..=(chunk + 1) * CHUNK {
223
            harness
224
                .inject
225
                .send(Event::MediaUploadProgress {
226
                    room_id: harness.room_id.clone(),
227
                    transaction_id: transaction_id.clone(),
228
                    uploaded_bytes,
229
                    total_bytes: CHUNKS * CHUNK,
230
                    complete: false,
231
                })
232
                .unwrap();
233
        }
234
        followers.push(tokio::spawn(follow(
235
            harness.mirror.subscribe(),
236
            transaction_id.clone(),
237
            CHUNKS * CHUNK,
238
        )));
239
    }
240
    for follower in followers {
241
        follower.await.unwrap();
242
    }
243
}
244
245
async fn follow(mut updates: Subscription, transaction_id: OwnedTransactionId, total: u64) {
246
    let first = updates.next().await.unwrap();
247
    let StateChange::State(state) = &first.change else {
248
        panic!("a subscription opened with {first:?}");
249
    };
250
    let mut seen = state
251
        .uploads
252
        .get(&transaction_id)
253
        .map_or(0, |upload| upload.uploaded_bytes);
254
    let mut sequence = first.sequence;
255
    while seen < total {
256
        let update = next_matching(&mut updates, |_| true).await;
257
        assert_eq!(update.sequence, sequence + 1);
258
        sequence = update.sequence;
259
        if let StateChange::Upload { upload, .. } = update.change {
260
            assert_eq!(upload.uploaded_bytes, seen + 1);
261
            seen += 1;
262
        }
263
    }
264
}
265
266
#[tokio::test]
267
async fn lagging_subscriber_starts_over_from_a_fresh_state() {
268
    let harness = start(EngineConfig::default(), 4).await;
269
    let transaction_id = TransactionId::new();
270
    let mut updates = harness.mirror.subscribe();
271
    assert!(matches!(
272
        updates.next().await.unwrap().change,
273
        StateChange::State(_)
274
    ));
275
    for uploaded_bytes in 1..=20 {
276
        harness
277
            .inject
278
            .send(Event::MediaUploadProgress {
279
                room_id: harness.room_id.clone(),
280
                transaction_id: transaction_id.clone(),
281
                uploaded_bytes,
282
                total_bytes: 20,
283
                complete: uploaded_bytes == 20,
284
            })
285
            .unwrap();
286
    }
287
    tokio::time::timeout(Duration::from_secs(5), async {
288
        while !harness
289
            .mirror
290
            .snapshot()
291
            .uploads
292
            .get(&transaction_id)
293
            .is_some_and(|upload| upload.complete)
294
        {
295
            tokio::task::yield_now().await;
296
        }
297
    })
298
    .await
299
    .expect("the mirror never caught up");
300
301
    let update = updates.next().await.unwrap();
302
    let StateChange::State(state) = &update.change else {
303
        panic!("a lagged subscription carried on with {update:?}");
304
    };
305
    assert!(state.uploads[&transaction_id].complete);
306
}
307
308
#[tokio::test]
309
async fn resync_does_not_deadlock_on_full_queues() {
310
    let config = EngineConfig::default()
311
        .with_command_capacity(1)
312
        .with_event_capacity(1);
313
    let harness = start(config, 1024).await;
314
    let timeline_id = TimelineId::from(harness.room_id.clone());
315
    let mut updates = harness.mirror.subscribe();
316
    harness
317
        .handle
318
        .open_room(harness.room_id.clone())
319
        .await
320
        .unwrap();
321
    next_matching(&mut updates, |change| {
322
        timeline_change(change, &timeline_id, |change| {
323
            matches!(change, TimelineChange::Reset { .. })
324
        })
325
    })
326
    .await;
327
328
    let flood = tokio::spawn({
329
        let handle = harness.handle.clone();
330
        let timeline_id = timeline_id.clone();
331
        async move {
332
            loop {
333
                let command = Command::Timeline(TimelineCommand::Paginate {
334
                    timeline_id: timeline_id.clone(),
335
                    limit: 1,
336
                });
337
                if handle.send(command).await.is_err() {
338
                    return;
339
                }
340
            }
341
        }
342
    });
343
    for _ in 0..10 {
344
        harness
345
            .inject
346
            .send(Event::TimelineChanged {
347
                timeline_id: timeline_id.clone(),
348
                diffs: vec![TimelineDiff::Remove { index: 1000 }],
349
            })
350
            .unwrap();
351
        next_matching(&mut updates, |change| {
352
            timeline_change(change, &timeline_id, |change| {
353
                matches!(change, TimelineChange::Diverged)
354
            })
355
        })
356
        .await;
357
        next_matching(&mut updates, |change| {
358
            timeline_change(change, &timeline_id, |change| {
359
                matches!(change, TimelineChange::Reset { .. })
360
            })
361
        })
362
        .await;
363
    }
364
    flood.abort();
365
}
366
367
#[tokio::test]
368
async fn closing_a_timeline_evicts_it() {
369
    let harness = start(EngineConfig::default(), 256).await;
370
    let timeline_id = TimelineId::from(harness.room_id.clone());
371
    let mut updates = harness.mirror.subscribe();
372
    harness
373
        .handle
374
        .open_room(harness.room_id.clone())
375
        .await
376
        .unwrap();
377
    harness
378
        .handle
379
        .close_room(harness.room_id.clone())
380
        .await
381
        .unwrap();
382
    next_matching(&mut updates, |change| {
383
        timeline_change(change, &timeline_id, |change| {
384
            matches!(change, TimelineChange::Closed)
385
        })
386
    })
387
    .await;
388
    assert!(
389
        !harness
390
            .mirror
391
            .snapshot()
392
            .timelines
393
            .contains_key(&timeline_id)
394
    );
395
}
396
397
#[tokio::test]
398
async fn an_upload_ends_on_finished() {
399
    let harness = start(EngineConfig::default(), 256).await;
400
    let transaction_id = TransactionId::new();
401
    let mut updates = harness.mirror.subscribe();
402
    harness
403
        .inject
404
        .send(Event::MediaUploadProgress {
405
            room_id: harness.room_id.clone(),
406
            transaction_id: transaction_id.clone(),
407
            uploaded_bytes: 5,
408
            total_bytes: 10,
409
            complete: false,
410
        })
411
        .unwrap();
412
    next_matching(&mut updates, |change| {
413
        matches!(change, StateChange::Upload { .. })
414
    })
415
    .await;
416
    assert!(
417
        harness
418
            .mirror
419
            .snapshot()
420
            .uploads
421
            .contains_key(&transaction_id)
422
    );
423
    harness
424
        .inject
425
        .send(Event::MediaUploadFinished {
426
            room_id: harness.room_id.clone(),
427
            transaction_id: transaction_id.clone(),
428
            outcome: UploadOutcome::Cancelled,
429
        })
430
        .unwrap();
431
    next_matching(&mut updates, |change| {
432
        matches!(change, StateChange::UploadFinished { .. })
433
    })
434
    .await;
435
    assert!(harness.mirror.snapshot().uploads.is_empty());
436
}
437
438
#[tokio::test]
439
async fn mentions_updates_require_the_current_account_and_revision() {
440
    let harness = start(EngineConfig::default(), 256).await;
441
    let mut updates = harness.mirror.subscribe();
442
    let account = tokio::time::timeout(Duration::from_secs(5), async {
443
        loop {
444
            let state = harness.mirror.snapshot();
445
            if state.mentions.is_some() {
446
                break state.user_id.clone().unwrap();
447
            }
448
            updates.next().await.unwrap();
449
        }
450
    })
451
    .await
452
    .expect("the initial mentions snapshot did not arrive");
453
    let mentions_update = |account, revision| {
454
        Event::MentionsChanged(MentionsUpdate {
455
            snapshot: MentionsPage {
456
                entries: Vec::new(),
457
                next_cursor: None,
458
                end_reached: true,
459
                coverage: MentionCoverage::default(),
460
                unread_count: 0,
461
                revision,
462
                boundary: MentionsBoundary { account, revision },
463
            },
464
            upserts: Vec::new(),
465
            removed: Vec::new(),
466
        })
467
    };
468
469
    harness
470
        .inject
471
        .send(mentions_update(account.clone(), 100))
472
        .unwrap();
473
    next_matching(
474
        &mut updates,
475
        |change| matches!(change, StateChange::Mentions(update) if update.snapshot.revision == 100),
476
    )
477
    .await;
478
    assert_eq!(
479
        harness
480
            .mirror
481
            .snapshot()
482
            .mentions
483
            .as_ref()
484
            .unwrap()
485
            .revision,
486
        100
487
    );
488
489
    harness
490
        .inject
491
        .send(mentions_update(account.clone(), 99))
492
        .unwrap();
493
    harness
494
        .inject
495
        .send(mentions_update(
496
            user_id!("@other:localhost").to_owned(),
497
            101,
498
        ))
499
        .unwrap();
500
    harness
501
        .inject
502
        .send(Event::ConnectionChanged(ConnectionState::Stopped))
503
        .unwrap();
504
    next_matching(&mut updates, |change| {
505
        matches!(change, StateChange::Connection(ConnectionState::Stopped))
506
    })
507
    .await;
508
    assert_eq!(
509
        harness
510
            .mirror
511
            .snapshot()
512
            .mentions
513
            .as_ref()
514
            .unwrap()
515
            .revision,
516
        100
517
    );
518
519
    harness.inject.send(Event::LoggedOut).unwrap();
520
    harness.inject.send(mentions_update(account, 102)).unwrap();
521
    harness
522
        .inject
523
        .send(Event::ConnectionChanged(ConnectionState::Stopped))
524
        .unwrap();
525
    next_matching(&mut updates, |change| {
526
        matches!(change, StateChange::Connection(ConnectionState::Stopped))
527
    })
528
    .await;
529
    assert!(harness.mirror.snapshot().mentions.is_none());
530
}