1
This commit is contained in:
@@ -20,15 +20,27 @@ Stream<List<int>> _delayedChunks() async* {
|
||||
yield [5, 6, 7, 8];
|
||||
}
|
||||
|
||||
Map<String, dynamic> _item() => {
|
||||
'datasetId': 7,
|
||||
'datasetName': '环颈雉鸡数据集',
|
||||
Map<String, dynamic> _item({
|
||||
int datasetId = 7,
|
||||
String variant = kVariantS,
|
||||
String name = '数据集A',
|
||||
}) =>
|
||||
{
|
||||
'datasetId': datasetId,
|
||||
'datasetName': name,
|
||||
if (variant.isNotEmpty) 'variant': variant,
|
||||
'version': 'v1.0.0',
|
||||
'labels': ['pheasant', 'suspect'],
|
||||
'labels': ['target', 'suspect'],
|
||||
'sizeBytes': _modelBytes.length,
|
||||
'sha256': _shaHex(_modelBytes),
|
||||
'downloadUrl': '/download/models/7/latest.tflite',
|
||||
'coverUrl': '/api/v1/app/cover?namePrefix=RNPHE',
|
||||
'downloadUrl': '/download/models/$datasetId/$variant.tflite',
|
||||
'coverUrl': '/api/v1/app/cover?namePrefix=DS001',
|
||||
};
|
||||
|
||||
Map<String, dynamic> _catalog(List<Map<String, dynamic>> models) => {
|
||||
'code': 0,
|
||||
'message': 'ok',
|
||||
'data': {'models': models}
|
||||
};
|
||||
|
||||
/// 真实流式下载客户端:send 立即返回分块流(MockClient 的 Response.fromStream
|
||||
@@ -38,11 +50,8 @@ class _StreamingClient extends http.BaseClient {
|
||||
Future<http.StreamedResponse> send(http.BaseRequest request) async {
|
||||
if (request.url.path == '/api/v1/app/update') {
|
||||
return http.StreamedResponse(
|
||||
http.ByteStream.fromBytes(utf8.encode(jsonEncode({
|
||||
'code': 0,
|
||||
'message': 'ok',
|
||||
'data': {'models': [_item()]}
|
||||
}))),
|
||||
http.ByteStream.fromBytes(utf8.encode(
|
||||
jsonEncode(_catalog([_item()])))),
|
||||
200);
|
||||
}
|
||||
return http.StreamedResponse(_delayedChunks(), 200,
|
||||
@@ -59,17 +68,12 @@ void main() {
|
||||
|
||||
tearDown(() => root.delete(recursive: true));
|
||||
|
||||
ModelManager manager() => ModelManager(
|
||||
ModelManager manager(List<Map<String, dynamic>> models) => ModelManager(
|
||||
baseUrl: 'http://test.local',
|
||||
client: MockClient((req) async {
|
||||
if (req.url.path == '/api/v1/app/update') {
|
||||
return http.Response.bytes(
|
||||
utf8.encode(jsonEncode({
|
||||
'code': 0,
|
||||
'message': 'ok',
|
||||
'data': {'models': [_item()]}
|
||||
})),
|
||||
200);
|
||||
utf8.encode(jsonEncode(_catalog(models))), 200);
|
||||
}
|
||||
if (req.url.path.startsWith('/download/models/')) {
|
||||
return http.Response.bytes(_modelBytes, 200);
|
||||
@@ -99,44 +103,55 @@ void main() {
|
||||
}
|
||||
|
||||
Future<void> pumpSection(WidgetTester tester, ModelManager m) async {
|
||||
// 与设置弹层一致:区块置于可滚动容器(区块含识别模式行后超出测试视口,
|
||||
// 点按前需 ensureVisible 滚动到目标)
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: Scaffold(body: ModelCatalogSection(manager: m))));
|
||||
home: Scaffold(
|
||||
body: SingleChildScrollView(
|
||||
child: ModelCatalogSection(manager: m)))));
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 50),
|
||||
EnginePhase.sendSemanticsUpdate, const Duration(seconds: 5));
|
||||
}
|
||||
|
||||
/// 滚动到目标可见后点按(区块可超出视口)
|
||||
Future<void> tapVisible(WidgetTester tester, Finder finder) async {
|
||||
await tester.ensureVisible(finder);
|
||||
await tester.pump();
|
||||
await tester.tap(finder);
|
||||
}
|
||||
|
||||
testWidgets('未下载:显示使用按钮,点击后下载完成自动变为已使用', (tester) async {
|
||||
final m = manager();
|
||||
final m = manager([_item()]);
|
||||
await tester.runAsync(() => m.refresh());
|
||||
await pumpSection(tester, m);
|
||||
|
||||
expect(find.text('环颈雉鸡数据集'), findsOneWidget);
|
||||
expect(find.text('数据集A'), findsOneWidget);
|
||||
expect(find.text('使用'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('使用'));
|
||||
await tapVisible(tester, find.text('使用'));
|
||||
await tester.pump(); // 下载启动,进度条出现
|
||||
// 驱动下载 + 自动激活的 IO 链走完,直至 UI 呈现「已使用」
|
||||
await pumpUntilFound(tester, find.text('已使用'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(m.isDownloaded(7), isTrue);
|
||||
expect(m.isActive(7), isTrue);
|
||||
expect(m.isDownloaded(7, kVariantS), isTrue);
|
||||
expect(m.isActive(7, kVariantS), isTrue);
|
||||
expect(find.text('已使用'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('已激活:再次点击取消使用', (tester) async {
|
||||
final m = manager();
|
||||
final m = manager([_item()]);
|
||||
await tester.runAsync(() => m.refresh());
|
||||
await tester.runAsync(() => m.downloadModel(m.catalog.first));
|
||||
await pumpSection(tester, m);
|
||||
|
||||
expect(find.text('已使用'), findsOneWidget);
|
||||
await tester.tap(find.text('已使用'));
|
||||
await tapVisible(tester, find.text('已使用'));
|
||||
// 排空 setActive 的激活集重载与落盘 IO,直至 UI 呈现「使用」
|
||||
await pumpUntilFound(tester, find.text('使用'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(m.isActive(7), isFalse);
|
||||
expect(m.isActive(7, kVariantS), isFalse);
|
||||
expect(find.text('使用'), findsOneWidget);
|
||||
});
|
||||
|
||||
@@ -144,8 +159,7 @@ void main() {
|
||||
final m = ModelManager(
|
||||
baseUrl: 'http://test.local',
|
||||
client: MockClient((_) async => http.Response.bytes(
|
||||
utf8.encode(jsonEncode(
|
||||
{'code': 0, 'message': 'ok', 'data': {'models': []}})),
|
||||
utf8.encode(jsonEncode(_catalog([]))),
|
||||
200)),
|
||||
rootDir: () async => root,
|
||||
);
|
||||
@@ -164,20 +178,83 @@ void main() {
|
||||
await tester.runAsync(() => m.refresh());
|
||||
await pumpSection(tester, m);
|
||||
|
||||
await tester.tap(find.text('使用'));
|
||||
await tapVisible(tester, find.text('使用'));
|
||||
// 首个分块到达,进度条与取消按钮出现(IO 链需交替驱动)
|
||||
await pumpUntilFound(tester, find.text('取消'),
|
||||
fake: const Duration(milliseconds: 400));
|
||||
expect(find.text('取消'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('取消'));
|
||||
await tapVisible(tester, find.text('取消'));
|
||||
// 推进 fake 时钟触发延迟分块 → 取消分支清场(.part 删除等真实 IO),直至恢复「使用」
|
||||
await pumpUntilFound(tester, find.text('使用'),
|
||||
fake: const Duration(milliseconds: 400));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(m.isDownloaded(7), isFalse);
|
||||
expect(m.isActive(7), isFalse);
|
||||
expect(m.isDownloaded(7, kVariantS), isFalse);
|
||||
expect(m.isActive(7, kVariantS), isFalse);
|
||||
expect(find.text('使用'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('双档位:s/n 两卡独立,n 档下载备好后切档即用', (tester) async {
|
||||
final m =
|
||||
manager([_item(), _item(variant: kVariantN, name: '数据集B')]);
|
||||
await tester.runAsync(() => m.refresh());
|
||||
await pumpSection(tester, m);
|
||||
|
||||
// 分段控件默认 s;两卡并排,s 卡按钮「使用」,非当前档 n 卡按钮「下载」
|
||||
expect(find.text('s 高识别'), findsOneWidget);
|
||||
expect(find.text('数据集A'), findsOneWidget);
|
||||
expect(find.text('数据集B'), findsOneWidget);
|
||||
expect(find.text('s'), findsOneWidget, reason: '档位角标');
|
||||
expect(find.text('n'), findsOneWidget, reason: '档位角标');
|
||||
expect(find.text('使用'), findsOneWidget);
|
||||
expect(find.text('下载'), findsOneWidget, reason: '非当前档入口为「下载」');
|
||||
|
||||
// 先激活 s 档
|
||||
await tapVisible(tester, find.text('使用'));
|
||||
await pumpUntilFound(tester, find.text('已使用'));
|
||||
expect(m.isActive(7, kVariantS), isTrue);
|
||||
|
||||
// 下载 n 档:完成后自动备好(n 非当前档),显示「已备好」而非「已使用」
|
||||
await tapVisible(tester, find.text('下载'));
|
||||
await pumpUntilFound(tester, find.text('已备好'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(m.isActive(7, kVariantN), isTrue);
|
||||
expect(m.mode, kVariantS, reason: '下载非当前档不应改变识别档位');
|
||||
expect(m.models.length, 1);
|
||||
expect(m.models.first.variant, kVariantS, reason: '当前仍只加载 s 档');
|
||||
|
||||
// 点「已备好」切换档位 → n 档立即生效(已使用),s 档转为备好态
|
||||
await tapVisible(tester, find.text('已备好'));
|
||||
// 「已使用」切换前后都在场(s 使用 → n 使用),须等 manager 状态真正切档
|
||||
for (var i = 0;
|
||||
i < 50 &&
|
||||
!(m.mode == kVariantN &&
|
||||
m.models.isNotEmpty &&
|
||||
m.models.first.variant == kVariantN);
|
||||
i++) {
|
||||
await pumpRealIo(tester);
|
||||
}
|
||||
await tester.pumpAndSettle();
|
||||
expect(m.mode, kVariantN);
|
||||
expect(m.models.length, 1);
|
||||
expect(m.models.first.variant, kVariantN, reason: '切档后热加载 n 档模型');
|
||||
expect(find.text('已使用'), findsOneWidget, reason: 'n 档卡已使用');
|
||||
expect(find.text('已备好'), findsOneWidget, reason: 's 档转为备好态');
|
||||
});
|
||||
|
||||
testWidgets('识别模式分段控件:点击切档生效', (tester) async {
|
||||
final m = manager([_item(), _item(variant: kVariantN, name: '数据集A')]);
|
||||
await tester.runAsync(() => m.refresh());
|
||||
await pumpSection(tester, m);
|
||||
|
||||
await tapVisible(tester, find.text('n 高性能'));
|
||||
for (var i = 0; i < 50 && m.mode != kVariantN; i++) {
|
||||
await pumpRealIo(tester);
|
||||
}
|
||||
await tester.pumpAndSettle();
|
||||
expect(m.mode, kVariantN, reason: '点击分段控件应切档');
|
||||
// 档位持久化(重启恢复)由 model_manager_test「识别档位持久化」覆盖;
|
||||
// widget 测试内 tap 触发的文件写与真实 IO 交错不可控,不在本层断言重启
|
||||
});
|
||||
}
|
||||
|
||||
@@ -26,19 +26,21 @@ Map<String, dynamic> _catalog(List<Map<String, dynamic>> models) => {
|
||||
|
||||
Map<String, dynamic> _item({
|
||||
int datasetId = 7,
|
||||
String name = '环颈雉鸡数据集',
|
||||
String variant = kVariantS,
|
||||
String name = '数据集A',
|
||||
String version = 'v1.0.0',
|
||||
String sha = '',
|
||||
}) =>
|
||||
{
|
||||
'datasetId': datasetId,
|
||||
'datasetName': name,
|
||||
if (variant.isNotEmpty) 'variant': variant,
|
||||
'version': version,
|
||||
'labels': ['pheasant', 'suspect'],
|
||||
'labels': ['target', 'suspect'],
|
||||
'sizeBytes': _modelBytes.length,
|
||||
'sha256': sha.isEmpty ? _shaHex(_modelBytes) : sha,
|
||||
'downloadUrl': '/download/models/$datasetId/latest.tflite',
|
||||
'coverUrl': '/api/v1/app/cover?namePrefix=RNPHE',
|
||||
'downloadUrl': '/download/models/$datasetId/$variant.tflite',
|
||||
'coverUrl': '/api/v1/app/cover?namePrefix=DS001',
|
||||
};
|
||||
|
||||
void main() {
|
||||
@@ -50,7 +52,20 @@ void main() {
|
||||
downloadHits = 0;
|
||||
});
|
||||
|
||||
tearDown(() => root.delete(recursive: true));
|
||||
tearDown(() async {
|
||||
// refresh 末尾的 autoUpdate 为不阻塞目录刷新的 fire-and-forget:其真实
|
||||
// 文件 IO 可能晚于 test body 结束,delete 撞上迟到写入会 Directory not
|
||||
// empty → 等待后重试清根(迟到链结束后即可删净)
|
||||
for (var i = 0; i < 40; i++) {
|
||||
try {
|
||||
await root.delete(recursive: true);
|
||||
return;
|
||||
} on FileSystemException {
|
||||
await Future<void>.delayed(const Duration(milliseconds: 10));
|
||||
}
|
||||
}
|
||||
await root.delete(recursive: true);
|
||||
});
|
||||
|
||||
ModelManager manager(MockClient client) => ModelManager(
|
||||
baseUrl: 'http://test.local',
|
||||
@@ -81,7 +96,9 @@ void main() {
|
||||
expect(downloadHits, 0, reason: 'refresh 不应触发下载');
|
||||
expect(m.models, isEmpty, reason: '未激活的模型不应出现在 models');
|
||||
expect(m.catalog.length, 1);
|
||||
expect(m.catalog.first.coverUrl, '/api/v1/app/cover?namePrefix=RNPHE');
|
||||
expect(m.catalog.first.coverUrl, '/api/v1/app/cover?namePrefix=DS001');
|
||||
expect(m.catalog.first.variant, kVariantS,
|
||||
reason: '旧目录无 variant 字段(单档 s)应归为 s');
|
||||
});
|
||||
|
||||
test('downloadModel:下载+校验+落盘+自动激活+进度回调', () async {
|
||||
@@ -95,11 +112,12 @@ void main() {
|
||||
|
||||
expect(ok, isTrue);
|
||||
expect(downloadHits, 1);
|
||||
expect(m.isDownloaded(7), isTrue);
|
||||
expect(m.isActive(7), isTrue, reason: '下载完成应自动使用');
|
||||
expect(m.isDownloaded(7, kVariantS), isTrue);
|
||||
expect(m.isActive(7, kVariantS), isTrue, reason: '下载完成应自动使用');
|
||||
expect(progresses.last, 1.0);
|
||||
expect(m.models.length, 1);
|
||||
expect(m.models.first.datasetName, '环颈雉鸡数据集');
|
||||
expect(m.models.first.datasetName, '数据集A');
|
||||
expect(m.models.first.variant, kVariantS);
|
||||
|
||||
final dir = Directory('${root.path}/7');
|
||||
expect(await File('${dir.path}/model.tflite').exists(), isTrue);
|
||||
@@ -111,13 +129,13 @@ void main() {
|
||||
final m = manager(client([_item()]));
|
||||
await m.refresh();
|
||||
await m.downloadModel(m.catalog.first);
|
||||
await m.setActive(7, false);
|
||||
expect(m.isActive(7), isFalse);
|
||||
await m.setActive(7, kVariantS, false);
|
||||
expect(m.isActive(7, kVariantS), isFalse);
|
||||
expect(m.models, isEmpty);
|
||||
|
||||
final before = downloadHits;
|
||||
await m.setActive(7, true);
|
||||
expect(m.isActive(7), isTrue);
|
||||
await m.setActive(7, kVariantS, true);
|
||||
expect(m.isActive(7, kVariantS), isTrue);
|
||||
expect(downloadHits, before, reason: '已下载直接使用不应重新下载');
|
||||
expect(m.models.length, 1);
|
||||
});
|
||||
@@ -149,7 +167,7 @@ void main() {
|
||||
}
|
||||
|
||||
expect(downloadHits, 2, reason: '新版本应自动重下');
|
||||
expect(m2.isActive(7), isTrue, reason: '自动更新应保持激活');
|
||||
expect(m2.isActive(7, kVariantS), isTrue, reason: '自动更新应保持激活');
|
||||
expect(m2.models.first.version, 'v2.0.0',
|
||||
reason: '自动更新后立即生效新版本字节');
|
||||
expect(m2.models.first.bytes, _modelBytes);
|
||||
@@ -166,9 +184,9 @@ void main() {
|
||||
final ok = await m2.downloadModel(m2.catalog.first);
|
||||
expect(ok, isFalse);
|
||||
expect(downloadHits, 2, reason: '校验失败应重试一次');
|
||||
expect(m2.isDownloaded(7), isFalse);
|
||||
expect(m2.isActive(7), isFalse);
|
||||
expect(m2.errorOf(7), isNotNull);
|
||||
expect(m2.isDownloaded(7, kVariantS), isFalse);
|
||||
expect(m2.isActive(7, kVariantS), isFalse);
|
||||
expect(m2.errorOf(7, kVariantS), isNotNull);
|
||||
});
|
||||
|
||||
test('激活集持久化:重启后恢复激活且已下载的模型', () async {
|
||||
@@ -179,7 +197,7 @@ void main() {
|
||||
// 同一 root 新建 manager 模拟重启
|
||||
final m2 = manager(client([_item()]));
|
||||
await m2.refresh();
|
||||
expect(m2.isActive(7), isTrue, reason: '激活集应持久化');
|
||||
expect(m2.isActive(7, kVariantS), isTrue, reason: '激活集应持久化');
|
||||
expect(m2.models.length, 1);
|
||||
expect(downloadHits, 1, reason: '重启不应触发下载');
|
||||
});
|
||||
@@ -193,14 +211,14 @@ void main() {
|
||||
final m2 = manager(client([]));
|
||||
await m2.refresh();
|
||||
expect(m2.models, isEmpty);
|
||||
expect(m2.isActive(7), isFalse, reason: '下线的模型应移出激活集');
|
||||
expect(m2.isActive(7, kVariantS), isFalse, reason: '下线的模型应移出激活集');
|
||||
expect(await Directory('${root.path}/7').exists(), isFalse,
|
||||
reason: '下线的数据集模型目录应被清理');
|
||||
});
|
||||
|
||||
test('多模型:只激活其一则只加载其一', () async {
|
||||
final m = manager(
|
||||
client([_item(datasetId: 7, name: '环颈雉'), _item(datasetId: 8, name: '斑鸠')]));
|
||||
client([_item(datasetId: 7, name: '数据集A'), _item(datasetId: 8, name: '数据集B')]));
|
||||
await m.refresh();
|
||||
await m.downloadModel(m.catalog.first); // 只下载并激活 7
|
||||
expect(m.models.length, 1);
|
||||
@@ -241,16 +259,90 @@ void main() {
|
||||
final fut = m.downloadModel(m.catalog.first);
|
||||
// 首个分块到达后取消(模拟用户在下载中点取消)
|
||||
await Future<void>.delayed(const Duration(milliseconds: 20));
|
||||
m.cancelDownload(7);
|
||||
m.cancelDownload(7, kVariantS);
|
||||
final ok = await fut;
|
||||
|
||||
expect(ok, isFalse);
|
||||
expect(downloadHits, 1);
|
||||
expect(m.isDownloaded(7), isFalse);
|
||||
expect(m.isActive(7), isFalse);
|
||||
expect(m.progressOf(7), isNull);
|
||||
expect(m.errorOf(7), isNull, reason: '取消不记错误');
|
||||
expect(m.isDownloaded(7, kVariantS), isFalse);
|
||||
expect(m.isActive(7, kVariantS), isFalse);
|
||||
expect(m.progressOf(7, kVariantS), isNull);
|
||||
expect(m.errorOf(7, kVariantS), isNull, reason: '取消不记错误');
|
||||
expect(await File('${root.path}/7/model.tflite.part').exists(), isFalse,
|
||||
reason: '取消后 .part 残留应被清理');
|
||||
});
|
||||
|
||||
test('双档位:n 档独立目录 models/7/n/,非当前档下载不加载', () async {
|
||||
final m = manager(client(
|
||||
[_item(), _item(variant: kVariantN, name: '数据集A', version: 'v1.0.0')]));
|
||||
await m.refresh();
|
||||
expect(m.catalog.length, 2);
|
||||
|
||||
// 默认 s 档:下载 s 自动激活并加载
|
||||
await m.downloadModel(m.catalog.first); // (7, s)
|
||||
expect(m.models.length, 1);
|
||||
expect(m.models.first.variant, kVariantS);
|
||||
|
||||
// 非当前档(n)下载:自动备好(激活保留),但 s 档加载列表不变
|
||||
final nItem = m.catalog.last;
|
||||
expect(nItem.variant, kVariantN);
|
||||
await m.downloadModel(nItem);
|
||||
expect(m.isDownloaded(7, kVariantN), isTrue);
|
||||
expect(m.isActive(7, kVariantN), isTrue, reason: '下载完成应自动备好');
|
||||
expect(m.models.length, 1, reason: '非当前档模型不应进入加载列表');
|
||||
expect(m.models.first.variant, kVariantS);
|
||||
|
||||
// n 档文件在独立子目录(目录键 = 档位),s 档仍为同级文件
|
||||
expect(await File('${root.path}/7/n/model.tflite').exists(), isTrue);
|
||||
expect(await File('${root.path}/7/n/meta.json').exists(), isTrue);
|
||||
expect(await File('${root.path}/7/model.tflite').exists(), isTrue,
|
||||
reason: 's 档 legacy 同级布局保持不变');
|
||||
|
||||
// 切档:热加载 n 档已备模型,s 档不再加载(无需重新下载)
|
||||
final before = downloadHits;
|
||||
await m.setMode(kVariantN);
|
||||
expect(m.mode, kVariantN);
|
||||
expect(downloadHits, before, reason: '切档不应触发下载');
|
||||
expect(m.models.length, 1);
|
||||
expect(m.models.first.variant, kVariantN);
|
||||
expect(m.models.first.datasetName, '数据集A');
|
||||
|
||||
// 切回 s 档恢复 s 模型
|
||||
await m.setMode(kVariantS);
|
||||
expect(m.models.length, 1);
|
||||
expect(m.models.first.variant, kVariantS);
|
||||
});
|
||||
|
||||
test('识别档位持久化:重启后保持档位并加载该档已激活模型', () async {
|
||||
final m = manager(client(
|
||||
[_item(), _item(variant: kVariantN, name: '数据集A', version: 'v1.0.0')]));
|
||||
await m.refresh();
|
||||
await m.downloadModel(m.catalog.first); // (7, s)
|
||||
await m.downloadModel(m.catalog.last); // (7, n) 备好
|
||||
await m.setMode(kVariantN);
|
||||
|
||||
// 同一 root 新建 manager 模拟重启:档位与激活集都持久化
|
||||
final m2 = manager(client(
|
||||
[_item(), _item(variant: kVariantN, name: '数据集A', version: 'v1.0.0')]));
|
||||
await m2.refresh();
|
||||
expect(m2.mode, kVariantN, reason: '识别档位应持久化');
|
||||
expect(m2.isActive(7, kVariantS), isTrue, reason: '激活集应持久化');
|
||||
expect(m2.models.length, 1);
|
||||
expect(m2.models.first.variant, kVariantN, reason: '重启后加载当前档位模型');
|
||||
expect(downloadHits, 2, reason: '重启与切档不应触发下载');
|
||||
});
|
||||
|
||||
test('数据集仍在但 n 档下线:清理 n 档子目录,保留数据集目录', () async {
|
||||
final m = manager(client([_item(variant: kVariantN)]));
|
||||
await m.refresh();
|
||||
await m.downloadModel(m.catalog.first); // (7, n)
|
||||
expect(await File('${root.path}/7/n/model.tflite').exists(), isTrue);
|
||||
|
||||
final m2 = manager(client([_item()])); // 目录只剩 s 档
|
||||
await m2.refresh();
|
||||
expect(await File('${root.path}/7/n/model.tflite').exists(), isFalse,
|
||||
reason: '数据集仍在但 n 档下线:应清理 n 档子目录');
|
||||
expect(await Directory('${root.path}/7').exists(), isTrue,
|
||||
reason: '数据集仍在(s 档):不应删整目录');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -17,32 +17,32 @@ DetectionResult box(String label, double score, double x, double y,
|
||||
|
||||
void main() {
|
||||
test('不同模型同标签重复框:NMS 去重取高分', () {
|
||||
// 环颈雉鸡模型与野兔模型都检出了同一只"环颈雉鸡"(不同模型对同一目标的重复框)
|
||||
// 两个模型都对同一目标检出同标签框(重复框需去重)
|
||||
final all = [
|
||||
box('pheasant', 0.18, 0.3, 0.3, modelId: 1, modelName: '环颈雉鸡模型'),
|
||||
box('pheasant', 0.55, 0.31, 0.3, modelId: 2, modelName: '野兔模型'),
|
||||
box('target', 0.18, 0.3, 0.3, modelId: 1, modelName: '模型A'),
|
||||
box('target', 0.55, 0.31, 0.3, modelId: 2, modelName: '模型B'),
|
||||
];
|
||||
final merged = mergeAcrossModels(all, 0.45);
|
||||
expect(merged.length, 1);
|
||||
expect(merged.first.score, 0.55);
|
||||
expect(merged.first.modelName, '野兔模型');
|
||||
expect(merged.first.modelName, '模型B');
|
||||
});
|
||||
|
||||
test('不同类别重叠:去重取高分(实测多模型对同一目标检异类别)', () {
|
||||
final all = [
|
||||
box('pheasant', 0.3, 0.5, 0.5, modelId: 1),
|
||||
box('hare', 0.7, 0.5, 0.5, modelId: 2), // 同位置但不同类别
|
||||
box('target', 0.3, 0.5, 0.5, modelId: 1),
|
||||
box('second', 0.7, 0.5, 0.5, modelId: 2), // 同位置但不同类别
|
||||
];
|
||||
final merged = mergeAcrossModels(all, 0.45);
|
||||
expect(merged.length, 1);
|
||||
expect(merged.first.label, 'hare');
|
||||
expect(merged.first.label, 'second');
|
||||
expect(merged.first.score, 0.7);
|
||||
});
|
||||
|
||||
test('不同类别不重叠:都保留', () {
|
||||
final all = [
|
||||
box('pheasant', 0.3, 0.1, 0.1, modelId: 1),
|
||||
box('hare', 0.7, 0.8, 0.8, modelId: 2), // 远处互不重叠
|
||||
box('target', 0.3, 0.1, 0.1, modelId: 1),
|
||||
box('second', 0.7, 0.8, 0.8, modelId: 2), // 远处互不重叠
|
||||
];
|
||||
final merged = mergeAcrossModels(all, 0.45);
|
||||
expect(merged.length, 2);
|
||||
@@ -50,8 +50,8 @@ void main() {
|
||||
|
||||
test('同模型内部与跨模型合并一致:远处不重叠保留', () {
|
||||
final all = [
|
||||
box('pheasant', 0.2, 0.1, 0.1, modelId: 1, modelName: '环颈雉鸡模型'),
|
||||
box('pheasant', 0.3, 0.8, 0.8, modelId: 1, modelName: '环颈雉鸡模型'),
|
||||
box('target', 0.2, 0.1, 0.1, modelId: 1, modelName: '模型A'),
|
||||
box('target', 0.3, 0.8, 0.8, modelId: 1, modelName: '模型A'),
|
||||
];
|
||||
final merged = mergeAcrossModels(all, 0.45);
|
||||
expect(merged.length, 2);
|
||||
|
||||
@@ -54,7 +54,7 @@ void main() {
|
||||
|
||||
test('centerInRegion_matches', () {
|
||||
final box = DetectionResult(
|
||||
label: 'hare',
|
||||
label: 'target',
|
||||
score: 0.30,
|
||||
left: 0.2,
|
||||
top: 0.3,
|
||||
|
||||
Reference in New Issue
Block a user