|
| 1 | +@Tags(['local-only', 'e2e']) |
| 2 | +@Timeout(Duration(minutes: 30)) |
| 3 | +/// Local-only chat app E2E for the model/mmproj download-cache-load path. |
| 4 | +/// |
| 5 | +/// This downloads the default LFM2-VL 450M model and its mmproj, so it is |
| 6 | +/// intentionally skipped by default. Run it manually with: |
| 7 | +/// |
| 8 | +/// ```bash |
| 9 | +/// cd example/chat_app |
| 10 | +/// flutter test --run-skipped -t local-only \ |
| 11 | +/// integration_test/model_cache_mmproj_e2e_test.dart -d <device> |
| 12 | +/// ``` |
| 13 | +library; |
| 14 | + |
| 15 | +import 'package:dio/dio.dart'; |
| 16 | +import 'package:flutter/foundation.dart'; |
| 17 | +import 'package:flutter_test/flutter_test.dart'; |
| 18 | +import 'package:integration_test/integration_test.dart'; |
| 19 | +import 'package:llamadart/llamadart.dart' show GpuBackend, LlamaLogLevel; |
| 20 | +import 'package:path/path.dart' as p; |
| 21 | + |
| 22 | +import 'package:llamadart_chat_example/models/chat_settings.dart'; |
| 23 | +import 'package:llamadart_chat_example/models/downloadable_model.dart'; |
| 24 | +import 'package:llamadart_chat_example/services/chat_service.dart'; |
| 25 | +import 'package:llamadart_chat_example/services/model_service_base.dart'; |
| 26 | + |
| 27 | +void main() { |
| 28 | + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); |
| 29 | + |
| 30 | + testWidgets( |
| 31 | + 'downloads, caches, and loads tiny multimodal model + mmproj', |
| 32 | + (tester) async { |
| 33 | + final model = DownloadableModel.defaultModels.firstWhere( |
| 34 | + (candidate) => candidate.name == 'LFM2-VL 450M', |
| 35 | + ); |
| 36 | + expect(model.multimodalProjectorSource, isNotNull); |
| 37 | + |
| 38 | + final service = ModelService(); |
| 39 | + final modelsDir = await service.getModelsDirectory(); |
| 40 | + |
| 41 | + await service.deleteModel(modelsDir, model); |
| 42 | + var downloaded = await service.getDownloadedModels([model]); |
| 43 | + expect(downloaded.contains(model.filename), isFalse); |
| 44 | + |
| 45 | + final stages = <ModelDownloadStage>{}; |
| 46 | + final progressEvents = <ModelDownloadProgress>[]; |
| 47 | + Object? downloadError; |
| 48 | + String? successFilename; |
| 49 | + |
| 50 | + await service.downloadModel( |
| 51 | + model: model, |
| 52 | + modelsDir: modelsDir, |
| 53 | + cancelToken: CancelToken(), |
| 54 | + onProgress: (_) {}, |
| 55 | + onProgressDetail: (detail) { |
| 56 | + stages.add(detail.stage); |
| 57 | + progressEvents.add(detail); |
| 58 | + debugPrint( |
| 59 | + 'E2E download ${detail.stage.name} ' |
| 60 | + '${(detail.overallProgress * 100).toStringAsFixed(1)}% ' |
| 61 | + '${detail.stageDownloadedBytes}/${detail.stageTotalBytes ?? -1}', |
| 62 | + ); |
| 63 | + }, |
| 64 | + onSuccess: (filename) { |
| 65 | + successFilename = filename; |
| 66 | + }, |
| 67 | + onError: (error) { |
| 68 | + downloadError = error; |
| 69 | + }, |
| 70 | + ); |
| 71 | + |
| 72 | + expect(downloadError, isNull); |
| 73 | + expect(successFilename, model.filename); |
| 74 | + expect(stages, contains(ModelDownloadStage.model)); |
| 75 | + expect(stages, contains(ModelDownloadStage.multimodalProjector)); |
| 76 | + expect(progressEvents.last.overallProgress, 1.0); |
| 77 | + |
| 78 | + downloaded = await service.getDownloadedModels([model]); |
| 79 | + expect(downloaded, contains(model.filename)); |
| 80 | + |
| 81 | + final modelSource = model.modelSource; |
| 82 | + final mmprojSource = model.multimodalProjectorSource!; |
| 83 | + final modelLoadRef = kIsWeb || modelSource is LocalModelAssetSource |
| 84 | + ? modelSource.loadReference |
| 85 | + : p.join(modelsDir, model.filename); |
| 86 | + final mmprojLoadRef = kIsWeb || mmprojSource is LocalModelAssetSource |
| 87 | + ? mmprojSource.loadReference |
| 88 | + : p.join(modelsDir, mmprojSource.displayName); |
| 89 | + |
| 90 | + final chatService = ChatService(); |
| 91 | + try { |
| 92 | + await chatService.init( |
| 93 | + ChatSettings( |
| 94 | + modelPath: modelLoadRef, |
| 95 | + mmprojPath: mmprojLoadRef, |
| 96 | + preferredBackend: GpuBackend.cpu, |
| 97 | + gpuLayers: 0, |
| 98 | + contextSize: 512, |
| 99 | + maxTokens: 32, |
| 100 | + nativeLogLevel: LlamaLogLevel.warn, |
| 101 | + ), |
| 102 | + eagerLoadMultimodalProjector: true, |
| 103 | + onProgress: (progress) => |
| 104 | + debugPrint('E2E load ${(progress * 100).toStringAsFixed(1)}%'), |
| 105 | + ); |
| 106 | + |
| 107 | + expect(chatService.engine.isReady, isTrue); |
| 108 | + expect(await chatService.engine.supportsVision, isTrue); |
| 109 | + } finally { |
| 110 | + await chatService.dispose(); |
| 111 | + } |
| 112 | + }, |
| 113 | + timeout: const Timeout(Duration(minutes: 30)), |
| 114 | + ); |
| 115 | +} |
0 commit comments