Files
pj_supermarket_cloud/test/face_sync_plan_test.dart
dzc 7c20242ad7 perf: optimize face library startup and differential sync
Stream and validate native feature loading with fast decoding and bounded SDK batches.
Reconcile authoritative department snapshots with rollback-safe metadata and file updates.
Bound downloads, isolate duplicate identifiers, and defer unchanged invalid photo versions.
Remove unused legacy face APIs and document device measurements.

Validation: 42 Flutter tests in an isolated source snapshot; native unit and device checks;
all 5,537 registered features verified byte-for-byte, plus real SDK photo searches and metadata checks.
2026-10-22 16:35:52 +08:00

137 lines
5.0 KiB
Dart

import 'dart:async';
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:pj_supermarket_cloud/common/index.dart';
import 'package:pj_supermarket_cloud/common/utils/sync/face_sync_plan.dart';
import 'package:pj_supermarket_cloud/common/utils/sync/face_image_download_queue.dart';
import 'package:pj_supermarket_cloud/common/services/database/type_converter/face_timestamp_converter.dart';
ServiceFaceSyncRecord record(String id, {DateTime? version, String? name}) =>
ServiceFaceSyncRecord(
id: id,
sampleId: id,
userId: '20',
deptId: '10',
faceImg: 'photo',
sourceUpdatedAt: version,
username: name);
FaceEntity local(String id, {DateTime? version, String? deptName}) =>
FaceEntity(
sampleId: id,
userId: '20',
userName: '',
deptId: '10',
deptName: deptName,
imagePath: 'photo',
path: '/$id.jpg',
faceModel: [1],
updateTime: version);
void main() {
test('重复同步与空部门名直接跳过,缺失版本只需校验一次照片', () {
final remote = [record('1'), record('2', version: DateTime(2026))];
final first = FaceSyncPlan.build(
remote: remote,
local: [local('1'), local('2')],
existingPaths: {'/1.jpg', '/2.jpg'});
expect(first.changes.map((x) => x.kind),
[FaceSyncChangeKind.unchanged, FaceSyncChangeKind.image]);
final second = FaceSyncPlan.build(
remote: remote,
local: [local('1'), local('2', version: DateTime(2026))],
existingPaths: {'/1.jpg', '/2.jpg'});
expect(second.changes.every((x) => x.kind == FaceSyncChangeKind.unchanged),
isTrue);
});
test('新增、照片版本变化和文件缺失只处理对应样本', () {
final plan = FaceSyncPlan.build(
remote: [record('1'), record('2', version: DateTime(2026)), record('3')],
local: [local('1'), local('2', version: DateTime(2025))],
existingPaths: {'/2.jpg'},
);
expect(
plan.changes.every((x) => x.kind == FaceSyncChangeKind.image), isTrue);
});
test('空部门权威清单可删除最后一张照片,并保留调试导入的外部门样本', () {
final foreign = local('2')..deptId = '99';
final plan = FaceSyncPlan.build(
remote: [],
local: [local('1'), foreign],
existingPaths: {},
scopeDeptIds: {'10', '11'});
expect(plan.orphans.map((x) => x.sampleId), ['1']);
});
test('冲突编号被保护,本地和远端重复清单会拒绝执行', () {
final plan = FaceSyncPlan.build(
remote: [],
local: [local('1'), local('2')],
existingPaths: {},
protectedSampleIds: {'1'});
expect(plan.orphans.map((x) => x.sampleId), ['2']);
expect(
() => FaceSyncPlan.build(
remote: [record('1'), record('1')], local: [], existingPaths: {}),
throwsA(isA<FaceSyncSnapshotException>()));
expect(
() => FaceSyncPlan.build(
remote: [], local: [local('1'), local('1')], existingPaths: {}),
throwsA(isA<FaceSyncSnapshotException>()));
});
test('时间版本保留微秒和时区,旧秒级格式仍可读取', () {
final converter = FaceTimestampConverter();
final version = DateTime.parse('2026-10-22T08:00:01.123456Z');
expect(converter.decode(converter.encode(version)), version);
expect(converter.decode('2026-10-22 08:00:01'),
DateTime(2026, 10, 22, 8, 0, 1));
final plan = FaceSyncPlan.build(
remote: [record('1', version: version)],
local: [local('1', version: version.toLocal())],
existingPaths: {'/1.jpg'});
expect(plan.changes.single.kind, FaceSyncChangeKind.unchanged);
});
test('消费未完成的预下载会等待同一任务,预取窗口始终有界', () async {
final tasks = <String, Completer<File>>{};
final calls = <String>[];
final discarded = <String>[];
final queue = FaceImageDownloadQueue(
records: List.generate(30, (i) => record('${i + 1}')),
download: (face) {
calls.add(face.sampleId);
return (tasks[face.sampleId] = Completer<File>()).future;
},
discard: (file) async => discarded.add(file.path),
);
expect(calls, ['1', '2', '3']);
final take = queue.take('1');
expect(calls.length, 3);
tasks['1']!.complete(File('/1'));
expect((await take)!.path, '/1');
expect(calls, ['1', '2', '3', '4']);
final closing = queue.dispose();
for (final id in ['2', '3', '4']) {
tasks[id]!.complete(File('/$id'));
}
await closing;
expect(calls.length, 4);
expect(discarded.toSet(), {'/2', '/3', '/4'});
expect(queue.consumedCount, 1);
});
test('下载异常被消费方接管,不产生未处理异步异常', () async {
final queue = FaceImageDownloadQueue(
records: [record('1')],
download: (_) async => throw const SocketException('offline'),
discard: (_) async {});
expect(await queue.take('1'), isNull);
await queue.dispose();
expect(queue.downloadCount, 1);
});
}