373 lines
11 KiB
Dart
373 lines
11 KiB
Dart
import 'dart:io';
|
||
|
||
import 'package:flutter_test/flutter_test.dart';
|
||
import 'package:path/path.dart' as p;
|
||
import 'package:path_provider_platform_interface/path_provider_platform_interface.dart';
|
||
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
||
import 'package:yx_tracking_flutter/src/model/device_info.dart';
|
||
import 'package:yx_tracking_flutter/src/model/event.dart';
|
||
import 'package:yx_tracking_flutter/src/model/system_dim_info.dart';
|
||
import 'package:yx_tracking_flutter/src/storage/db_constants.dart';
|
||
import 'package:yx_tracking_flutter/src/storage/sqflite_config_storage.dart';
|
||
import 'package:yx_tracking_flutter/src/storage/sqflite_event_storage.dart';
|
||
|
||
class FakePathProviderPlatform extends PathProviderPlatform {
|
||
FakePathProviderPlatform(this.documentsPath);
|
||
|
||
final String documentsPath;
|
||
|
||
@override
|
||
Future<String?> getApplicationDocumentsPath() async => documentsPath;
|
||
}
|
||
|
||
void main() {
|
||
sqfliteFfiInit();
|
||
final ffiFactory = databaseFactoryFfi;
|
||
|
||
group('SqfliteEventStorage (ffi)', () {
|
||
late Directory tempDir;
|
||
late PathProviderPlatform originalPathProvider;
|
||
|
||
setUp(() {
|
||
originalPathProvider = PathProviderPlatform.instance;
|
||
tempDir = Directory.systemTemp.createTempSync('yx_tracking_event_');
|
||
});
|
||
|
||
tearDown(() {
|
||
PathProviderPlatform.instance = originalPathProvider;
|
||
if (tempDir.existsSync()) {
|
||
tempDir.deleteSync(recursive: true);
|
||
}
|
||
});
|
||
|
||
SqfliteEventStorage makeStorage() {
|
||
return SqfliteEventStorage(
|
||
databaseFactory: ffiFactory,
|
||
documentsDirectoryProvider: () async => tempDir,
|
||
);
|
||
}
|
||
|
||
Event makeEvent(String type, int ts) {
|
||
return Event(
|
||
systemCode: 'SYS',
|
||
eventType: type,
|
||
userInfo: null,
|
||
clientType: 3,
|
||
clientTimestamp: ts,
|
||
timestamp: '2026-01-01T00:00:00.000Z',
|
||
deviceInfo: const DeviceInfo(
|
||
os: 'os',
|
||
model: 'model',
|
||
screenResolution: '1x1',
|
||
),
|
||
eventParams: <String, Object?>{'i': ts},
|
||
customTags: const <String, Object?>{'t': 1},
|
||
createTime: DateTime.fromMillisecondsSinceEpoch(ts),
|
||
);
|
||
}
|
||
|
||
test('未 init 时会抛 StateError', () {
|
||
final storage = makeStorage();
|
||
expect(storage.count, throwsStateError);
|
||
});
|
||
|
||
test('基础 CRUD + limit<=0 分支 + init 幂等', () async {
|
||
final storage = makeStorage();
|
||
|
||
await storage.init();
|
||
await storage.init();
|
||
|
||
expect(await storage.fetchBatch(0), isEmpty);
|
||
expect(await storage.fetchRecent(0), isEmpty);
|
||
|
||
final id1 = await storage.insert(makeEvent('A', 1));
|
||
final id2 = await storage.insert(makeEvent('B', 2));
|
||
|
||
expect(id1, greaterThan(0));
|
||
expect(id2, greaterThan(id1));
|
||
|
||
final batch = await storage.fetchBatch(10);
|
||
expect(batch.map((e) => e.event.eventType), <String>['A', 'B']);
|
||
|
||
final recent = await storage.fetchRecent(10);
|
||
expect(recent.map((e) => e.event.eventType), <String>['B', 'A']);
|
||
|
||
await storage.deleteByIds(const <int>[]);
|
||
await storage.updateRetryCount(id1, 1);
|
||
|
||
await storage.dispose();
|
||
await storage.dispose();
|
||
});
|
||
|
||
test('trimToMaxSize 会删除最旧事件并返回删除数', () async {
|
||
final storage = makeStorage();
|
||
await storage.init();
|
||
|
||
await storage.insert(makeEvent('A', 1));
|
||
await storage.insert(makeEvent('B', 2));
|
||
await storage.insert(makeEvent('C', 3));
|
||
|
||
final trimmed = await storage.trimToMaxSize(2);
|
||
expect(trimmed, 1);
|
||
|
||
final left = await storage.fetchBatch(10);
|
||
expect(left.map((e) => e.event.eventType), <String>['B', 'C']);
|
||
});
|
||
|
||
test('deleteExpired 会删除截止时间之前的事件', () async {
|
||
final storage = makeStorage();
|
||
await storage.init();
|
||
|
||
await storage.insert(makeEvent('OLD', 1));
|
||
await storage.insert(makeEvent('NEW', 100));
|
||
|
||
final removed = await storage.deleteExpired(
|
||
DateTime.fromMillisecondsSinceEpoch(50),
|
||
);
|
||
expect(removed, 1);
|
||
|
||
final left = await storage.fetchBatch(10);
|
||
expect(left.map((e) => e.event.eventType), <String>['NEW']);
|
||
});
|
||
|
||
test('updateRetryCount 会同时更新 stored 与 event.retryCount', () async {
|
||
final storage = makeStorage();
|
||
await storage.init();
|
||
|
||
final id = await storage.insert(makeEvent('R', 10));
|
||
final stored = (await storage.fetchBatch(1)).single;
|
||
expect(stored.retryCount, 0);
|
||
expect(stored.event.retryCount, 0);
|
||
|
||
await storage.updateRetryCount(id, 2);
|
||
|
||
final updated = (await storage.fetchBatch(1)).single;
|
||
expect(updated.retryCount, 2);
|
||
expect(updated.event.retryCount, 2);
|
||
});
|
||
|
||
test('坏数据会被清理避免卡死队列', () async {
|
||
final storage = makeStorage();
|
||
await storage.init();
|
||
|
||
await storage.insert(makeEvent('GOOD', 1));
|
||
|
||
final db = storage.debugDb!;
|
||
await db.insert(
|
||
'events',
|
||
<String, Object?>{
|
||
'payload': 'not-json',
|
||
'retry_count': 0,
|
||
'create_time': 2,
|
||
},
|
||
);
|
||
|
||
final before = await storage.count();
|
||
final batch = await storage.fetchBatch(10);
|
||
final after = await storage.count();
|
||
|
||
expect(before, 2);
|
||
expect(batch.map((e) => e.event.eventType), <String>['GOOD']);
|
||
expect(after, 1);
|
||
});
|
||
|
||
test('默认 documents provider 分支可工作(使用 fake 平台)', () async {
|
||
PathProviderPlatform.instance = FakePathProviderPlatform(tempDir.path);
|
||
final storage = SqfliteEventStorage(databaseFactory: ffiFactory);
|
||
await storage.init();
|
||
|
||
expect(await storage.count(), 0);
|
||
});
|
||
|
||
test('onUpgrade(oldVersion<1) 会建表', () async {
|
||
final dbPath = p.join(tempDir.path, DbConstants.dbName);
|
||
final db = await ffiFactory.openDatabase(
|
||
dbPath,
|
||
options: OpenDatabaseOptions(version: 1),
|
||
);
|
||
await db.execute('PRAGMA user_version = 0;');
|
||
await db.close();
|
||
|
||
final storage = makeStorage();
|
||
await storage.init();
|
||
expect(await storage.count(), 0);
|
||
});
|
||
|
||
test('deleteByIds 多 id 会走占位符分支', () async {
|
||
final storage = makeStorage();
|
||
await storage.init();
|
||
|
||
final id1 = await storage.insert(makeEvent('D1', 1));
|
||
final id2 = await storage.insert(makeEvent('D2', 2));
|
||
|
||
await storage.deleteByIds(<int>[id1, id2]);
|
||
expect(await storage.count(), 0);
|
||
});
|
||
|
||
test('行字段类型异常会被清理(create_time 非 int)', () async {
|
||
final storage = makeStorage();
|
||
await storage.init();
|
||
|
||
final db = storage.debugDb!;
|
||
await db.insert(
|
||
'events',
|
||
<String, Object?>{
|
||
'payload': '{}',
|
||
'retry_count': 0,
|
||
'create_time': 'bad-int',
|
||
},
|
||
);
|
||
|
||
final before = await storage.count();
|
||
final batch = await storage.fetchBatch(10);
|
||
final after = await storage.count();
|
||
|
||
expect(before, 1);
|
||
expect(batch, isEmpty);
|
||
expect(after, 0);
|
||
});
|
||
});
|
||
|
||
group('SqfliteConfigStorage (ffi)', () {
|
||
late Directory tempDir;
|
||
late PathProviderPlatform originalPathProvider;
|
||
|
||
setUp(() {
|
||
originalPathProvider = PathProviderPlatform.instance;
|
||
tempDir = Directory.systemTemp.createTempSync('yx_tracking_config_');
|
||
});
|
||
|
||
tearDown(() {
|
||
PathProviderPlatform.instance = originalPathProvider;
|
||
if (tempDir.existsSync()) {
|
||
tempDir.deleteSync(recursive: true);
|
||
}
|
||
});
|
||
|
||
SqfliteConfigStorage makeStorage() {
|
||
return SqfliteConfigStorage(
|
||
databaseFactory: ffiFactory,
|
||
documentsDirectoryProvider: () async => tempDir,
|
||
);
|
||
}
|
||
|
||
SystemDimInfo makeInfo(int ts) {
|
||
return SystemDimInfo(
|
||
systemInfo: const SystemInfo(raw: <String, Object?>{'a': 1}),
|
||
eventDefinitions: const <EventDefinition>[
|
||
EventDefinition(eventCode: 'E'),
|
||
],
|
||
tagDefinitions: const <TagDefinition>[
|
||
TagDefinition(tagName: 't', tagType: 'string', isRequired: true),
|
||
],
|
||
sdkStrategy: const SdkStrategy(
|
||
enabled: true,
|
||
defaultSampleRate: 1,
|
||
eventSettings: <String, EventStrategy>{},
|
||
),
|
||
lastFetchedAt: DateTime.fromMillisecondsSinceEpoch(ts),
|
||
version: 'v',
|
||
);
|
||
}
|
||
|
||
test('未 init 时会抛 StateError', () {
|
||
final storage = makeStorage();
|
||
expect(storage.loadSystemDimInfo, throwsStateError);
|
||
});
|
||
|
||
test('save/load/clear/lastFetchedAt 正常工作', () async {
|
||
final storage = makeStorage();
|
||
await storage.init();
|
||
|
||
expect(await storage.loadSystemDimInfo(), isNull);
|
||
|
||
final info = makeInfo(123);
|
||
await storage.saveSystemDimInfo(info);
|
||
|
||
final loaded = await storage.loadSystemDimInfo();
|
||
expect(loaded?.systemInfo?.raw['a'], 1);
|
||
expect(loaded?.eventDefinitions.single.eventCode, 'E');
|
||
final rows = await storage.debugDb!.query('config_cache');
|
||
expect(
|
||
rows.single['last_fetched_at'],
|
||
info.lastFetchedAt.millisecondsSinceEpoch,
|
||
);
|
||
|
||
await storage.clear();
|
||
expect(await storage.loadSystemDimInfo(), isNull);
|
||
|
||
await storage.dispose();
|
||
await storage.dispose();
|
||
});
|
||
|
||
test('默认 documents provider 分支可工作(使用 fake 平台)', () async {
|
||
PathProviderPlatform.instance = FakePathProviderPlatform(tempDir.path);
|
||
final storage = SqfliteConfigStorage(databaseFactory: ffiFactory);
|
||
await storage.init();
|
||
|
||
expect(await storage.loadSystemDimInfo(), isNull);
|
||
});
|
||
|
||
test('payload 为空字符串会被清理', () async {
|
||
final storage = makeStorage();
|
||
await storage.init();
|
||
|
||
final db = storage.debugDb!;
|
||
await db.insert(
|
||
'config_cache',
|
||
<String, Object?>{
|
||
'key': 'system_dim_info',
|
||
'payload': '',
|
||
'last_fetched_at': 1,
|
||
},
|
||
);
|
||
|
||
final loaded = await storage.loadSystemDimInfo();
|
||
expect(loaded, isNull);
|
||
expect(await db.query('config_cache'), isEmpty);
|
||
});
|
||
|
||
test('payload 为非 Map JSON 会被清理', () async {
|
||
final storage = makeStorage();
|
||
await storage.init();
|
||
|
||
final db = storage.debugDb!;
|
||
await db.insert(
|
||
'config_cache',
|
||
<String, Object?>{
|
||
'key': 'system_dim_info',
|
||
'payload': '[]',
|
||
'last_fetched_at': 1,
|
||
},
|
||
);
|
||
|
||
final loaded = await storage.loadSystemDimInfo();
|
||
expect(loaded, isNull);
|
||
expect(await db.query('config_cache'), isEmpty);
|
||
});
|
||
|
||
test('坏配置会被删除并返回 null', () async {
|
||
final storage = makeStorage();
|
||
await storage.init();
|
||
|
||
final dbPath = p.join(tempDir.path, DbConstants.dbName);
|
||
final db = storage.debugDb!;
|
||
|
||
await db.insert(
|
||
'config_cache',
|
||
<String, Object?>{
|
||
'key': 'system_dim_info',
|
||
'payload': 'not-json',
|
||
'last_fetched_at': 1,
|
||
},
|
||
);
|
||
|
||
final loaded = await storage.loadSystemDimInfo();
|
||
expect(loaded, isNull);
|
||
|
||
final rows = await db.query('config_cache');
|
||
expect(rows, isEmpty, reason: '坏数据应被清理: $dbPath');
|
||
});
|
||
});
|
||
}
|