1
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:crypto/crypto.dart' show sha256;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/testing.dart';
|
||||
import 'package:observer/camera/model_catalog_section.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();
|
||||
|
||||
/// 分两段到达的下载流(中间 50ms 停顿,供取消测试在下载中触发)
|
||||
Stream<List<int>> _delayedChunks() async* {
|
||||
yield [1, 2, 3, 4];
|
||||
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||||
yield [5, 6, 7, 8];
|
||||
}
|
||||
|
||||
Map<String, dynamic> _item() => {
|
||||
'datasetId': 7,
|
||||
'datasetName': '环颈雉鸡数据集',
|
||||
'version': 'v1.0.0',
|
||||
'labels': ['pheasant', 'suspect'],
|
||||
'sizeBytes': _modelBytes.length,
|
||||
'sha256': _shaHex(_modelBytes),
|
||||
'downloadUrl': '/download/models/7/latest.tflite',
|
||||
'coverUrl': '/api/v1/app/cover?namePrefix=RNPHE',
|
||||
};
|
||||
|
||||
void main() {
|
||||
late Directory root;
|
||||
|
||||
setUp(() async {
|
||||
root = await Directory.systemTemp.createTemp('catalog-section-test');
|
||||
});
|
||||
|
||||
tearDown(() => root.delete(recursive: true));
|
||||
|
||||
ModelManager manager() => 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);
|
||||
}
|
||||
if (req.url.path.startsWith('/download/models/')) {
|
||||
return http.Response.bytes(_modelBytes, 200);
|
||||
}
|
||||
return http.Response('not found', 404);
|
||||
}),
|
||||
rootDir: () async => root,
|
||||
);
|
||||
|
||||
Future<void> pumpSection(WidgetTester tester, ModelManager m) async {
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: Scaffold(
|
||||
body: ModelCatalogSection(manager: m))));
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
testWidgets('未下载:显示使用按钮,点击后下载完成自动变为已使用', (tester) async {
|
||||
final m = manager();
|
||||
await m.refresh();
|
||||
await pumpSection(tester, m);
|
||||
|
||||
expect(find.text('环颈雉鸡数据集'), findsOneWidget);
|
||||
expect(find.text('使用'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('使用'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(m.isDownloaded(7), isTrue);
|
||||
expect(m.isActive(7), isTrue);
|
||||
expect(find.text('已使用'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('已激活:再次点击取消使用', (tester) async {
|
||||
final m = manager();
|
||||
await m.refresh();
|
||||
await m.downloadModel(m.catalog.first);
|
||||
await pumpSection(tester, m);
|
||||
|
||||
expect(find.text('已使用'), findsOneWidget);
|
||||
await tester.tap(find.text('已使用'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(m.isActive(7), isFalse);
|
||||
expect(find.text('使用'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('目录为空:显示暂无已发布模型', (tester) async {
|
||||
final m = ModelManager(
|
||||
baseUrl: 'http://test.local',
|
||||
client: MockClient((_) async => http.Response.bytes(
|
||||
utf8.encode(jsonEncode(
|
||||
{'code': 0, 'message': 'ok', 'data': {'models': []}})),
|
||||
200)),
|
||||
rootDir: () async => root,
|
||||
);
|
||||
await m.refresh();
|
||||
await pumpSection(tester, m);
|
||||
|
||||
expect(find.text('暂无已发布模型'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('下载中:显示进度和取消按钮,取消后恢复使用', (tester) async {
|
||||
final m = 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);
|
||||
}
|
||||
if (req.url.path.startsWith('/download/models/')) {
|
||||
return http.Response.fromStream(http.StreamedResponse(
|
||||
_delayedChunks(), 200,
|
||||
contentLength: _modelBytes.length));
|
||||
}
|
||||
return http.Response('not found', 404);
|
||||
}),
|
||||
rootDir: () async => root,
|
||||
);
|
||||
await m.refresh();
|
||||
await pumpSection(tester, m);
|
||||
|
||||
await tester.tap(find.text('使用'));
|
||||
await tester.pump(); // 首个分块到达,进度条与取消按钮出现
|
||||
expect(find.text('取消'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('取消'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(m.isDownloaded(7), isFalse);
|
||||
expect(m.isActive(7), isFalse);
|
||||
expect(find.text('使用'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
@@ -11,6 +11,13 @@ const _modelBytes = [1, 2, 3, 4, 5, 6, 7, 8];
|
||||
|
||||
String _shaHex(List<int> bytes) => sha256.convert(bytes).toString();
|
||||
|
||||
/// 分两段到达的下载流(中间 50ms 停顿,供取消测试在下载中触发)
|
||||
Stream<List<int>> _delayedChunks() async* {
|
||||
yield [1, 2, 3, 4];
|
||||
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||||
yield [5, 6, 7, 8];
|
||||
}
|
||||
|
||||
Map<String, dynamic> _catalog(List<Map<String, dynamic>> models) => {
|
||||
'code': 0,
|
||||
'message': 'ok',
|
||||
@@ -194,4 +201,36 @@ void main() {
|
||||
expect(broken.ready, isFalse);
|
||||
expect(broken.error, isNotNull);
|
||||
});
|
||||
|
||||
test('取消下载:中断后恢复未下载、不记错误', () async {
|
||||
final m = manager(MockClient((req) async {
|
||||
if (req.url.path == '/api/v1/app/update') {
|
||||
return http.Response.bytes(
|
||||
utf8.encode(jsonEncode(_catalog([_item()]))), 200);
|
||||
}
|
||||
if (req.url.path.startsWith('/download/models/')) {
|
||||
downloadHits++;
|
||||
return http.Response.fromStream(http.StreamedResponse(
|
||||
_delayedChunks(), 200,
|
||||
contentLength: _modelBytes.length));
|
||||
}
|
||||
return http.Response('not found', 404);
|
||||
}));
|
||||
await m.refresh();
|
||||
|
||||
final fut = m.downloadModel(m.catalog.first);
|
||||
// 首个分块到达后取消(模拟用户在下载中点取消)
|
||||
await Future<void>.delayed(const Duration(milliseconds: 20));
|
||||
m.cancelDownload(7);
|
||||
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(await File('${root.path}/7/model.tflite.part').exists(), isFalse,
|
||||
reason: '取消后 .part 残留应被清理');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:crypto/crypto.dart' show sha256;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/testing.dart';
|
||||
import 'package:observer/camera/model_catalog_section.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> _item() => {
|
||||
'datasetId': 7,
|
||||
'datasetName': '环颈雉鸡数据集',
|
||||
'version': 'v1.0.0',
|
||||
'labels': ['pheasant', 'suspect'],
|
||||
'sizeBytes': _modelBytes.length,
|
||||
'sha256': _shaHex(_modelBytes),
|
||||
'downloadUrl': '/download/models/7/latest.tflite',
|
||||
'coverUrl': '/api/v1/app/cover?namePrefix=RNPHE',
|
||||
};
|
||||
|
||||
void main() {
|
||||
late Directory root;
|
||||
|
||||
setUp(() async {
|
||||
root = await Directory.systemTemp.createTemp('catalog-diag');
|
||||
});
|
||||
|
||||
tearDown(() => root.delete(recursive: true));
|
||||
|
||||
ModelManager manager() => 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);
|
||||
}
|
||||
if (req.url.path.startsWith('/download/models/')) {
|
||||
return http.Response.bytes(_modelBytes, 200);
|
||||
}
|
||||
return http.Response('not found', 404);
|
||||
}),
|
||||
rootDir: () async => root,
|
||||
);
|
||||
|
||||
testWidgets('diag step marker', (tester) async {
|
||||
print('=== BEFORE manager()');
|
||||
final m = manager();
|
||||
print('=== BEFORE refresh()');
|
||||
await m.refresh();
|
||||
print('=== AFTER refresh() ready=${m.ready} catalog=${m.catalog.length}');
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: Scaffold(body: ModelCatalogSection(manager: m))));
|
||||
print('=== AFTER pumpWidget');
|
||||
for (var i = 0; i < 15; i++) {
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
final spinners = find.byType(CircularProgressIndicator).evaluate().length;
|
||||
final icons = find.byIcon(Icons.image_not_supported_outlined).evaluate().length;
|
||||
print('pump $i scheduled=${tester.binding.hasScheduledFrame} '
|
||||
'spinner=$spinners errIcon=$icons');
|
||||
}
|
||||
print('=== bounded pumpAndSettle attempt');
|
||||
try {
|
||||
await tester.pumpAndSettle(
|
||||
const Duration(milliseconds: 100),
|
||||
EnginePhase.sendSemanticsUpdate,
|
||||
const Duration(seconds: 5));
|
||||
print('=== pumpAndSettle OK');
|
||||
} catch (e) {
|
||||
print('=== pumpAndSettle TIMEOUT: $e');
|
||||
}
|
||||
print('=== DONE');
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user