Files
observer/flutter_app/test/model_manager_test.dart
T
admin a0b115d954 训练体系整合与标注单阶段化
- 标注:AI 预标注直写 labels_json(去候选确认两阶段);重叠去重(minIoU);全量标注按钮
- 训练:脚本迁移入 server/training/(Go 化 prepare_yolo/analyze_rfdetr,保留 train_server.py);tflite 产物自检并入训练流程(check_tflite)
- 数据目录/权重不进 git;.gitignore 迁移至仓库根
2026-08-26 18:22:56 +08:00

142 lines
4.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:convert';
import 'dart:io';
import 'package:crypto/crypto.dart' show sha256;
import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart' as http;
import 'package:http/testing.dart';
import 'package:observer/models/model_manager.dart';
const _modelBytes = [1, 2, 3, 4, 5, 6, 7, 8];
String _shaHex(List<int> bytes) => sha256.convert(bytes).toString();
Map<String, dynamic> _catalog(List<Map<String, dynamic>> models) =>
{'code': 0, 'message': 'ok', 'data': {'version': '0.0.2', 'notes': '', 'models': models}};
Map<String, dynamic> _item({String version = 'v1.0.0', String sha = ''}) => {
'datasetId': 7,
'datasetName': '野鸡数据集',
'version': version,
'labels': ['pheasant', 'suspect'],
'sizeBytes': _modelBytes.length,
'sha256': sha.isEmpty ? _shaHex(_modelBytes) : sha,
'downloadUrl': '/download/models/野鸡数据集/latest.tflite',
};
void main() {
late Directory root;
late int downloadHits;
setUp(() async {
root = await Directory.systemTemp.createTemp('model-manager-test');
downloadHits = 0;
});
tearDown(() => root.delete(recursive: true));
ModelManager manager(MockClient client) => ModelManager(
baseUrl: 'http://test.local',
client: client,
rootDir: () async => root,
);
MockClient client(List<Map<String, dynamic>> models) => MockClient((req) async {
if (req.url.path == '/api/v1/app/update') {
// Response(String) 默认 latin1 编码,中文数据集名会抛异常 → 用 bytes
return http.Response.bytes(utf8.encode(jsonEncode(_catalog(models))), 200);
}
if (req.url.path.startsWith('/download/models/')) {
downloadHits++;
return http.Response.bytes(_modelBytes, 200);
}
return http.Response('not found', 404);
});
test('首次拉取:下载模型并落盘(model/labels/meta', () async {
final m = manager(client([_item()]));
await m.refresh();
expect(m.ready, isTrue);
expect(m.error, isNull);
expect(m.models.length, 1);
expect(m.models.first.datasetName, '野鸡数据集');
expect(m.models.first.bytes, _modelBytes);
expect(downloadHits, 1);
final dir = Directory('${root.path}/7');
expect(await File('${dir.path}/model.tflite').exists(), isTrue);
expect(await File('${dir.path}/labels.json').exists(), isTrue);
expect(await File('${dir.path}/meta.json').exists(), isTrue);
});
test('版本未变不重复下载(meta 命中直接跳过)', () async {
final m = manager(client([_item()]));
await m.refresh();
expect(downloadHits, 1);
await m.refresh();
expect(downloadHits, 1, reason: 'meta 匹配应跳过下载');
expect(m.models.length, 1);
});
test('版本更新触发重新下载', () async {
final m = manager(client([_item()]));
await m.refresh();
expect(downloadHits, 1);
await m.refresh();
expect(downloadHits, 1);
// 发布新版本:再次刷新应重下
await m.refresh();
expect(downloadHits, 1);
// 上面三次同一版本,重新构造带新版本的 manager(同一 root
final m2 = manager(client([_item(version: 'v2.0.0')]));
await m2.refresh();
expect(downloadHits, 2);
expect(m2.models.first.version, 'v2.0.0');
});
test('sha256 不匹配:重试后失败,保留旧模型并报错', () async {
// 第一次下载成功(sha 匹配)
final m1 = manager(client([_item()]));
await m1.refresh();
expect(m1.models.length, 1);
// 服务器 sha 与文件不符(被篡改/损坏)→ 下载校验失败
final bad = _item(version: 'v3.0.0');
bad['sha256'] = _shaHex([9, 9, 9]);
final m2 = manager(client([bad]));
await m2.refresh();
expect(m2.models.length, 0, reason: '校验失败的模型不应加载');
expect(m2.error, isNotNull);
expect(m2.error, contains('野鸡数据集'));
});
test('目录下线:清理本地并清空模型', () async {
final m = manager(client([_item()]));
await m.refresh();
expect(m.models.length, 1);
expect(await Directory('${root.path}/7').exists(), isTrue);
final m2 = manager(client([]));
await m2.refresh();
expect(m2.models, isEmpty);
expect(await Directory('${root.path}/7').exists(), isFalse,
reason: '下线的数据集模型目录应被清理');
});
test('目录接口异常:不覆盖已有就绪状态', () async {
final m = manager(client([_item()]));
await m.refresh();
expect(m.ready, isTrue);
final broken = manager(MockClient((_) async => http.Response('boom', 500)));
await broken.refresh();
expect(broken.ready, isFalse);
expect(broken.error, isNotNull);
});
}