import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:image/image.dart' as img; import 'package:jj_mkvmaker/app/ai_local.dart'; import 'package:jj_mkvmaker/app/ai_upscale.dart'; import 'package:jj_mkvmaker/app/app_controller.dart'; import 'package:jj_mkvmaker/app/settings.dart'; import 'package:jj_mkvmaker/platform/windows/desktop_storage_service.dart'; import 'package:jj_mkvmaker/platform/windows/process_media_tool.dart'; import 'package:jj_mkvmaker/services/platform_services.dart'; import 'package:jj_mkvmaker/ui/ai_upscale_ui.dart'; import 'package:path/path.dart' as p; /// 176: 한 장 해상도 올리기도 작업 (AiJobs) 으로 - 작업 알림 · 앞 서비스 · 작업 현황에 나오고 화면을 떠나도 끝난다 void main() { late Directory tmp; late String input; late Completer engine; setUp(() { tmp = Directory.systemTemp.createTempSync('jj_up_job_'); input = p.join(tmp.path, 'page.png'); File(input).writeAsBytesSync(img.encodePng(img.Image(width: 8, height: 6))); engine = Completer(); debugUpscalerForTest = () => AiUpscaler( device: const SdDevice('sd-cli', 'cpu', 'CPU'), modelPath: 'm.pth', modelId: 'esrgan-x4plus', runner: (exe, args, {onProgress, onStart}) async { await engine.future; // 엔진이 도는 동안 File(args[args.indexOf('-o') + 1]).writeAsBytesSync(img.encodePng(img.Image(width: 32, height: 24))); return const []; }, ); }); tearDown(() { debugUpscalerForTest = null; AiJobs.instance.done(''); // 남은 [보기] 정리 tmp.deleteSync(recursive: true); }); test('176: 해상도 올리기 메모리 설정 - 적게 20% · 보통 30% (처음 값) · 많이 45%, 저장', () { expect(AppSettings().aiUpMemory, 'normal'); expect(AppSettings.fromJson((AppSettings()..aiUpMemory = 'high').toJson()).aiUpMemory, 'high'); expect(AppSettings.fromJson({'aiUpMemory': 'x'}).aiUpMemory, 'normal'); // S10 (약 15GB): 적게 320 · 보통 384 · 많이 512 expect([for (final m in ['low', 'normal', 'high']) androidUpscaleTile(memTotalMb: 15000, share: upscaleMemoryShare(m))], [320, 384, 512]); expect(upscaleTileMemoryMb(512), 6600); // 빠르기 기록은 메모리 설정마다 따로 (Android CPU 만) final a = AiUpscaler(device: const SdDevice('x', 'cpu', 'CPU'), modelPath: 'm', modelId: 'esrgan-x4plus', memory: 'low'); expect(upscaleSpeedKey(a), Platform.isAndroid ? 'cpu|esrgan-x4plus|low' : 'cpu|esrgan-x4plus'); }); test('타일 크기: 기기 메모리의 20% 안 (S10 16GB → 320), 메모리를 모르면 큰 그림만 256', () { expect(androidUpscaleTile(memTotalMb: 15000, share: 0.20), 320); expect(androidUpscaleTile(memTotalMb: 40000, share: 0.20), 512); expect(androidUpscaleTile(memTotalMb: 6000, share: 0.20), 192); expect(androidUpscaleTile(longSide: 1024), 256); // 192: 메모리가 작은 기기 (P11 4GB, MemTotal 약 3.5GB) 는 셋 다 가장 작은 타일 - 같은 값을 세 번 보이지 않고 같다고 알린다 final small = upscaleMemoryLines(3500); expect(small.where((l) => l.contains('모두 같습니다')), hasLength(1)); expect(small.where((l) => l.startsWith('적게') || l.startsWith('많이')), isEmpty); // 16GB 는 셋이 다르다 expect(upscaleMemoryLines(15000).where((l) => l.contains('같')), isEmpty); // 일부만 같으면 그 줄에 (보통과 같음) expect(upscaleMemoryLines(5000).where((l) => l.contains('(보통과 같음)')), isNotEmpty); expect(androidUpscaleTile(longSide: 800), 512); }); testWidgets('[이 장] 은 작업 목록에 오르고, [뒤에서 계속] 으로 화면을 떠나도 끝나며 [보기] 로 비교 · 저장', (tester) async { final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); var finished = false; await tester.pumpWidget(MaterialApp( home: Scaffold( body: Builder( builder: (ctx) => TextButton( onPressed: () => upscaleOne(ctx, c, input: input, nameFrom: input, outDir: tmp.path).whenComplete(() => finished = true), child: const Text('올리기'), ), ), ), )); await tester.tap(find.text('올리기')); for (var i = 0; i < 20 && !AiJobs.instance.busy; i++) { await tester.runAsync(() => Future.delayed(const Duration(milliseconds: 20))); await tester.pump(); } // 작업 목록 (알림 · 작업 현황) 에 오른다 expect(AiJobs.instance.busy, isTrue); expect(AiJobs.instance.statusLine, contains('page.png')); // 화면을 떠난다 (진행 창만 닫힘, 작업은 계속) await tester.tap(find.text('뒤에서 계속')); await tester.pumpAndSettle(); expect(find.text('AI 해상도 올리기'), findsNothing); expect(AiJobs.instance.busy, isTrue); // 엔진이 끝나면 비교 화면을 띄우지 않고 [보기] 를 남긴다 engine.complete(); // 시험의 가짜 시간 안에서 시작한 일이라 기다리지 말고 끝날 때까지 돌린다 for (var i = 0; i < 200 && !finished; i++) { await tester.runAsync(() => Future.delayed(const Duration(milliseconds: 20))); await tester.pump(); } expect(finished, isTrue); await tester.pumpAndSettle(); expect(AiJobs.instance.busy, isFalse); expect(AiJobs.instance.lastDone, contains('page.png')); expect(find.byType(UpscaleCompareView), findsNothing); final view = AiJobs.instance.lastDoneView; expect(view, isNotNull); // [보기] → 비교 화면 → [저장] var viewed = false; view!().whenComplete(() => viewed = true); await tester.pumpAndSettle(); expect(find.byType(UpscaleCompareView), findsOneWidget); await tester.runAsync(() => tester.tap(find.widgetWithText(FilledButton, '저장'))); for (var i = 0; i < 100 && !viewed; i++) { await tester.runAsync(() => Future.delayed(const Duration(milliseconds: 20))); await tester.pump(); } expect(viewed, isTrue); final saved = [for (final f in tmp.listSync(recursive: true).whereType()) if (!p.equals(f.path, input)) f.path]; expect(saved, isNotEmpty, reason: '저장됨 (원본 옆, 새 파일)'); expect(AiJobs.instance.lastDoneView, isNull, reason: '[보기] 는 한 번'); // 작업 현황 줄도 저장한 곳으로 ([열기] 가 그 폴더) expect(AiJobs.instance.lastDone, '저장했습니다: ${saved.single}'); expect(AiJobs.instance.lastDoneDir, p.dirname(saved.single)); ScaffoldMessenger.of(tester.element(find.text('올리기'))).clearSnackBars(); await tester.pumpAndSettle(); }); }