Files
pyramid/test/support/synthetic_matrix_data.dart
T
Bernd SteckmeisterandClaude Opus 5.5 d5ff7982a0 refactor: lokale Nachrichtensuche als eigenes Modul (lib/features/search) + 5 Vertragstests
Verhaltensgleich aus search_modal.dart herausgeloest (gleiche Deckel 1500/60/500,
gleiche Schleife; Medien-Filter jetzt Parameter). Tests laufen ueber die echte
SDK-DB-Schicht mit synthetischen Daten (test/support/synthetic_matrix_data.dart).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-07 11:55:28 +02:00

201 lines
5.7 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:math';
import 'package:matrix/matrix.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
/// Synthetische Matrix-Daten für Tests und `benchmark/` – komplett offline:
/// kein Homeserver, keine echten Konten, keine Secrets.
///
/// Die Daten gehen über die ECHTE SDK-Datenbankschicht (`MatrixSdkDatabase`,
/// `storeRoomUpdate`/`storeEventUpdate` in Sync-artigen Transaktionen) – also
/// denselben Weg wie beim echten Sync. Dadurch messen/prüfen Tests und
/// Benchmarks die Speicherstruktur, die die App wirklich auf der Platte hat.
class SyntheticMatrixData {
SyntheticMatrixData._();
static const homeserverDomain = 'bench.invalid';
static const _senders = 5;
/// Wörter für die Nachrichtentexte (bewusst mit Umlauten – die Suche
/// arbeitet mit `toLowerCase`).
static const _words = [
'hallo',
'morgen',
'treffen',
'kaffee',
'bahnhof',
'urlaub',
'foto',
'danke',
'später',
'gleich',
'abend',
'essen',
'kino',
'wetter',
'regen',
'sonne',
'arbeit',
'pause',
'auto',
'zug',
'buch',
'musik',
'spiel',
'garten',
'hund',
'katze',
'einkaufen',
'termin',
'grüße',
'übermorgen',
];
static String roomId(int r) => '!room$r:$homeserverDomain';
static String eventId(int r, int i) => '\$r${r}e$i';
static String sender(int i) => '@user${i % _senders}:$homeserverDomain';
/// Öffnet die SDK-Datenbank auf [db] und baut einen Offline-Client darauf.
static Future<Client> openClient(
Database db, {
String name = 'PyramidBench',
}) async {
final sdkDb = await MatrixSdkDatabase.init('pyramid', database: db);
return Client(name, database: sdkDb);
}
/// Schreibt [rooms] Räume mit je [eventsPerRoom] Nachrichten in die DB.
///
/// Wie beim echten Sync landen die Events in Transaktionen à [batchSize]
/// (≈ eine Sync-Antwort) und in zeitlicher Reihenfolge (älteste zuerst).
/// Jede 10. Nachricht ist ein Bild (`m.image`). Ist [needleEvery] gesetzt,
/// enthält jede n-te Nachricht zusätzlich das Wort [needle] – so kennen
/// Tests die exakte Trefferzahl der Suche.
static Future<void> populate(
Client client, {
required int rooms,
required int eventsPerRoom,
int batchSize = 50,
String needle = 'nadelwort',
int? needleEvery,
int seed = 42,
}) async {
final db = client.database;
final rnd = Random(seed);
final baseTs = DateTime.utc(2026, 1, 1).millisecondsSinceEpoch;
for (var r = 0; r < rooms; r++) {
final id = roomId(r);
await db.transaction(() async {
await db.storeRoomUpdate(
id,
JoinedRoomUpdate(
summary: RoomSummary.fromJson({
'm.joined_member_count': _senders,
'm.invited_member_count': 0,
}),
),
null,
client,
);
for (final state in _roomState(r)) {
await db.storeEventUpdate(id, state, EventUpdateType.state, client);
}
});
for (var start = 0; start < eventsPerRoom; start += batchSize) {
final end = min(start + batchSize, eventsPerRoom);
await db.transaction(() async {
for (var i = start; i < end; i++) {
final hasNeedle = needleEvery != null && i % needleEvery == 0;
await db.storeEventUpdate(
id,
_message(r, i, baseTs, rnd, hasNeedle ? needle : null),
EventUpdateType.timeline,
client,
);
}
});
}
}
}
/// Lädt die Raumliste aus der DB in `client.rooms` – das, was beim
/// App-Start `client.init()` macht, ohne Netzwerk/Login.
static Future<List<Room>> loadRooms(Client client) async {
final rooms = await client.database.getRoomList(client);
client.rooms
..clear()
..addAll(rooms);
return rooms;
}
static List<MatrixEvent> _roomState(int r) {
final ts = DateTime.utc(2025, 12, 1).millisecondsSinceEpoch;
MatrixEvent state(
String type,
String stateKey,
Map<String, Object?> content,
int n,
) => MatrixEvent(
type: type,
eventId: '\$state${r}_$n',
senderId: sender(0),
originServerTs: DateTime.fromMillisecondsSinceEpoch(ts + n),
stateKey: stateKey,
content: content,
roomId: roomId(r),
);
return [
state(EventTypes.RoomCreate, '', {'creator': sender(0)}, 0),
state(EventTypes.RoomName, '', {'name': 'Raum $r'}, 1),
for (var u = 0; u < _senders; u++)
state(EventTypes.RoomMember, sender(u), {
'membership': 'join',
'displayname': 'Nutzer $u',
}, 2 + u),
];
}
static MatrixEvent _message(
int r,
int i,
int baseTs,
Random rnd,
String? needle,
) {
final isImage = i % 10 == 9;
final Map<String, Object?> content;
if (isImage) {
content = {
'msgtype': 'm.image',
'body': 'IMG_${r}_$i.jpg',
'url': 'mxc://$homeserverDomain/img$r$i',
'info': {
'w': 1920,
'h': 1080,
'mimetype': 'image/jpeg',
'size': 250000,
},
};
} else {
final words = List.generate(
4 + rnd.nextInt(20),
(_) => _words[rnd.nextInt(_words.length)],
);
if (needle != null) words.insert(rnd.nextInt(words.length), needle);
content = {'msgtype': 'm.text', 'body': words.join(' ')};
}
return MatrixEvent(
type: EventTypes.Message,
eventId: eventId(r, i),
senderId: sender(i),
originServerTs: DateTime.fromMillisecondsSinceEpoch(
baseTs + r * 1000 + i * 60000,
),
content: content,
roomId: roomId(r),
);
}
}