feat(mobile): upload engine with preparation, batching states and retries

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-19 02:50:40 +08:00
parent d96e56f653
commit 3ddb0ea2d4
23 changed files with 3232 additions and 12 deletions
+119
View File
@@ -0,0 +1,119 @@
/// 设备标识(DeviceId)测试(P3 计划 Task 6)。
library;
import 'dart:math';
import 'package:flutter_test/flutter_test.dart';
import 'package:photo_live/core/device_id.dart';
class MemoryDeviceIdStore implements DeviceIdStore {
String? value;
int writes = 0;
@override
Future<String?> read() async => value;
@override
Future<void> write(String v) async {
writes++;
value = v;
}
}
void main() {
test('首次生成:32 位小写十六进制并持久化', () async {
final store = MemoryDeviceIdStore();
final id = await DeviceId(store: store).value();
expect(DeviceId.kPattern.hasMatch(id), isTrue, reason: '形状:$id');
expect(store.value, id);
expect(store.writes, 1);
});
test('懒缓存 + 跨实例稳定:同一存储只生成一次', () async {
final store = MemoryDeviceIdStore();
final a = DeviceId(store: store);
final b = DeviceId(store: store);
final first = await a.value();
// 同实例二次调用走缓存(不再写存储)。
expect(await a.value(), first);
expect(store.writes, 1);
// 新实例从存储读取同一值(per-install 语义)。
expect(await b.value(), first);
expect(store.writes, 1);
});
test('并发首次调用共享同一次生成(不重复落盘)', () async {
final store = MemoryDeviceIdStore();
final device = DeviceId(store: store);
final results = await Future.wait(
List.generate(5, (_) => device.value()),
);
expect(results.toSet().length, 1);
expect(store.writes, 1);
});
test('不同安装(不同存储)生成不同标识', () async {
final a = await DeviceId(store: MemoryDeviceIdStore()).value();
final b = await DeviceId(store: MemoryDeviceIdStore()).value();
expect(a, isNot(b));
});
test('存储中的非法值(长度/字符不符)重新生成', () async {
final store = MemoryDeviceIdStore()..value = 'short';
final id = await DeviceId(store: store).value();
expect(DeviceId.kPattern.hasMatch(id), isTrue);
expect(store.value, id); // 修正值写回
});
test('存储读取失败(异常)按未生成处理', () async {
final throwing = _ThrowingStore();
final id = await DeviceId(store: throwing).value();
expect(DeviceId.kPattern.hasMatch(id), isTrue);
});
test('hexEncode:字节 → 小写两位十六进制', () {
expect(
DeviceId.hexEncode([0x00, 0x0f, 0x10, 0xff, 0xa5]),
'000f10ffa5',
);
expect(DeviceId.hexEncode(List.filled(16, 0xab)).length, 32);
});
test('可注入 Random(确定性生成便于复现)', () async {
final fixed = _SequenceRandom();
final id = await DeviceId(store: MemoryDeviceIdStore(), random: fixed)
.value();
expect(id, '0102030405060708090a0b0c0d0e0f10');
});
}
class _ThrowingStore implements DeviceIdStore {
@override
Future<String?> read() async => throw _Boom();
@override
Future<void> write(String value) async {}
}
class _Boom implements Exception {}
/// 固定序列 Random(测试确定性注入)。
class _SequenceRandom implements Random {
final List<int> _values = List<int>.generate(16, (i) => i + 1);
int _cursor = 0;
@override
int nextInt(int max) {
final v = _values[_cursor % _values.length] % max;
_cursor++;
return v;
}
@override
bool nextBool() => true;
@override
double nextDouble() => .5;
}