diff --git a/integration_test/com_guard_test.dart b/integration_test/com_guard_test.dart index 7a05d7f..72df760 100644 --- a/integration_test/com_guard_test.dart +++ b/integration_test/com_guard_test.dart @@ -19,11 +19,11 @@ void main() { // ignore: avoid_print print('RESULT 처음 확인: $first (1 = 이미 준비되어 있음 → 같은 스레드)'); expect(first, 1); - ComGuard.releaseForTest(); // 방금 올린 만큼 내림 + ComGuard.release(); // 방금 올린 만큼 내림 // 2. 준비 상태를 끝까지 푼다 (문제 상황). 여러 구성 요소가 겹쳐 준비해 두므로 넉넉히 여러 번. for (var i = 0; i < 8; i++) { - ComGuard.releaseForTest(); + ComGuard.release(); } // Edge 가 앱이 끝날 때까지 사용자 데이터 폴더를 잡는다 (test_temp_check 가 찾음, 관리자 지시 10/20) final dir = testTempDir('jj_com_', heldByApp: true).path; @@ -41,7 +41,7 @@ void main() { // 3. 앱의 방식 (만들기 전에 다시 준비) 으로는 만들어진다 for (var i = 0; i < 8; i++) { - ComGuard.releaseForTest(); + ComGuard.release(); } final env = await tester.runAsync(() => browserEnvironment(dir)); // ignore: avoid_print diff --git a/integration_test/windows_vault_shot_test.dart b/integration_test/windows_vault_shot_test.dart index 4787056..d35c965 100644 --- a/integration_test/windows_vault_shot_test.dart +++ b/integration_test/windows_vault_shot_test.dart @@ -34,7 +34,7 @@ void main() { t.view.physicalSize = size; t.view.devicePixelRatio = 1; await t.runAsync(() => t.pumpWidget(RepaintBoundary( - key: key, child: MaterialApp(theme: buildTheme(), debugShowCheckedModeBanner: false, home: VaultPage(c: c))))); + key: key, child: MaterialApp(theme: buildTheme(), debugShowCheckedModeBanner: false, home: VaultPage(c: c, iterations: 1000))))); for (var i = 0; i < 15; i++) { await t.runAsync(() => Future.delayed(const Duration(milliseconds: 100))); await t.pump(const Duration(milliseconds: 50)); @@ -57,13 +57,12 @@ void main() { await v.add(f.path, folder: '여행'); await v.mkdir('빈 폴더'); }); - vaultIterationsForTest = 1000; for (final (name, size) in [('open_412', const Size(412, 800)), ('open_960', const Size(960, 600))]) { vaultDirOf = () async => dir; t.view.physicalSize = size; t.view.devicePixelRatio = 1; await t.runAsync(() => t.pumpWidget(RepaintBoundary( - key: key, child: MaterialApp(theme: buildTheme(), debugShowCheckedModeBanner: false, home: VaultPage(c: c))))); + key: key, child: MaterialApp(theme: buildTheme(), debugShowCheckedModeBanner: false, home: VaultPage(c: c, iterations: 1000))))); for (var i = 0; i < 10; i++) { await t.runAsync(() => Future.delayed(const Duration(milliseconds: 100))); await t.pump(const Duration(milliseconds: 50)); diff --git a/lib/core/shizuku.dart b/lib/core/shizuku.dart index bb7f56c..d7bb564 100644 --- a/lib/core/shizuku.dart +++ b/lib/core/shizuku.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; -import 'package:flutter/foundation.dart' show visibleForTesting; import 'package:flutter/services.dart'; import 'package:path/path.dart' as p; @@ -55,8 +54,8 @@ class SuRoot implements ShizukuBackend { static bool _granted = false; - @visibleForTesting - static void resetForTest() => _granted = false; + /// 이번 실행에서 받은 허락을 잊는다 (다음 요청에서 su 에 다시 묻는다) + static void forgetGrant() => _granted = false; Future<(int, String, String)> _run(String exe, List args, {Duration timeout = const Duration(seconds: 60)}) async { final p = await start(exe, args); diff --git a/lib/platform/windows/com_guard.dart b/lib/platform/windows/com_guard.dart index bacc81b..2fb4ecf 100644 --- a/lib/platform/windows/com_guard.dart +++ b/lib/platform/windows/com_guard.dart @@ -37,6 +37,8 @@ class ComGuard { return hr == 0; } - /// 시험용: 준비 상태를 한 단계 푼다 - static void releaseForTest() => _uninit(); + /// 준비 상태를 한 단계 푼다 ([ensure] 와 짝 - CoUninitialize) + static void release() { + if (Platform.isWindows) _uninit(); + } } diff --git a/lib/services/platform_services.dart b/lib/services/platform_services.dart index ce54207..362da07 100644 --- a/lib/services/platform_services.dart +++ b/lib/services/platform_services.dart @@ -4,6 +4,7 @@ import 'package:path/path.dart' as p; import '../platform/common/media_kit_full_player.dart'; import '../platform/common/media_kit_player.dart'; +import '../app/ai_upscale.dart' show AiUpscaler; import '../app/settings.dart'; import '../core/download_detect.dart'; import '../platform/common/nllb_translator.dart'; @@ -64,6 +65,9 @@ class PlatformServices { /// PC 의 CPU · 메모리 사용량 (없으면 표시 숨김) final SystemUsage? usage; + /// 엔진 · 모델을 이미 정해 둔 해상도 올리기 (없으면 설정대로 준비하고, 없는 엔진 · 모델은 그 자리에서 받는다) + final AiUpscaler Function()? createUpscaler; + /// 이 기기 (Android · Windows · 메모리) - 기기마다 다르게 도는 곳은 이것을 본다 (M1, 처음 값 = 이 기기) final PlatformInfo platform; @@ -79,6 +83,7 @@ class PlatformServices { this.createTranslator, this.createDownloadBackends, this.createSubtitleProviders, + this.createUpscaler, this.updater, this.usage, ModelStore? models, diff --git a/lib/ui/ai_upscale/upscale_prep.dart b/lib/ui/ai_upscale/upscale_prep.dart index 176b38c..34aa64f 100644 --- a/lib/ui/ai_upscale/upscale_prep.dart +++ b/lib/ui/ai_upscale/upscale_prep.dart @@ -54,13 +54,9 @@ Future<(String input, String nameFrom, String outDir)?> readerPageFile(AppContro return null; // PDF 등은 아직 } -/// 시험용: 받기 · 엔진 고르기 없이 쓸 업스케일러 (가짜 엔진) -@visibleForTesting -AiUpscaler Function()? debugUpscalerForTest; - /// 준비: 모델 · 엔진이 없으면 그 자리에서 무엇을 받을지 고르고 받는다 (156 - 설정 화면으로 보내지 않음). 못 하면 null Future readyUpscalerFor(BuildContext context, AppController c) async { - if (debugUpscalerForTest case final f?) return f(); + if (c.services.createUpscaler case final f?) return f(); final store = AiStore.instance ??= AiStore(Directory.systemTemp.path); Future ready() => prepareUpscaler(store, diff --git a/lib/ui/ai_upscale_ui.dart b/lib/ui/ai_upscale_ui.dart index 05a0cde..9f97b35 100644 --- a/lib/ui/ai_upscale_ui.dart +++ b/lib/ui/ai_upscale_ui.dart @@ -23,7 +23,7 @@ import 'ai_upscale/upscale_prep.dart'; import 'ai_upscale/upscale_compare.dart'; export 'ai_upscale/upscale_compare.dart' show UpscaleCompareView, UpscaleCompareViewState; -export 'ai_upscale/upscale_prep.dart' show debugUpscalerForTest, readerPageFile, upscaleOptionsOf; +export 'ai_upscale/upscale_prep.dart' show readerPageFile, upscaleOptionsOf; /// 121: 한 장 올리기 → 진행 (취소) → [원본 / 올린 것] 비교 → [저장] (새 파일, 원본을 덮지 않음). 저장한 경로 (안 하면 null) Future upscaleOne(BuildContext context, AppController c, {required String input, required String nameFrom, required String outDir}) async { diff --git a/lib/ui/remote_thumb_view.dart b/lib/ui/remote_thumb_view.dart index 7277924..5f35843 100644 --- a/lib/ui/remote_thumb_view.dart +++ b/lib/ui/remote_thumb_view.dart @@ -31,8 +31,8 @@ Future remoteThumbsOf(AppController c) => _hub ??= () async { return h; }(); -/// 시험: 다음에 새로 만든다 -void resetRemoteThumbsForTest() => _hub = null; +/// 썸네일 허브를 잊는다 (다음에 지금 설정 · 폴더로 새로 만든다) +void forgetRemoteThumbHub() => _hub = null; /// 원격 그림은 이보다 크면 받지 않는다 (썸네일 하나에 큰 파일을 통째로 받지 않게) const remoteThumbImageMax = 30 * 1024 * 1024; @@ -43,8 +43,8 @@ int remoteThumbVideoCap = 8 * 1024 * 1024; /// 서버마다 Range 를 따르는지 (이번 실행 동안 기억 - 앱을 다시 켜면 한 번 다시 봄) final _rangeOk = {}; -/// 시험: Range 기억을 지운다 -void resetRemoteThumbRangeForTest() => _rangeOk.clear(); +/// 서버마다 Range 를 따르는지 기억한 것을 지운다 (다음에 다시 물어본다) +void forgetRemoteRangeSupport() => _rangeOk.clear(); /// 동영상 썸네일을 만들어도 되는 원격인지 (28 지적 · 관리자 지시 10/21): /// - WebDAV: Range 를 모르는 서버 (206 이 아님) 는 뒤쪽 한 장면에 파일 전체를 받으므로 만들지 않음 (서버마다 기억, 물어보다 실패하면 기억하지 않음) @@ -89,8 +89,8 @@ Future remoteThumbsAllowedNow(AppController c) async { return !_metered; } -/// 시험: 망 판단 기억을 지운다 -void resetRemoteThumbNetForTest() => _meteredAt = null; +/// 망 판단 (데이터 요금제인지) 기억을 지운다 (다음에 다시 본다) +void forgetRemoteThumbNet() => _meteredAt = null; /// FFmpeg 가 이 기기 안 스트리밍 다리 (127.0.0.1, Range) 에서 필요한 부분만 읽어 가로 160 jpg 로. 비밀번호는 명령 줄에 넣지 않는다. /// MKV 만들기 등 작업 중에는 만들지 않는다 (FFmpeg 를 같이 씀) diff --git a/lib/ui/vault_page.dart b/lib/ui/vault_page.dart index cd3dcb4..43c7269 100644 --- a/lib/ui/vault_page.dart +++ b/lib/ui/vault_page.dart @@ -37,9 +37,6 @@ Future Function() vaultDirOf = () async { /// 꺼낼 폴더 고르기 (시험에서 바꿈) Future Function() vaultPickFolder = () => FilePicker.getDirectoryPath(); -/// 시험: PBKDF2 반복 수 (새 보관함 · 비밀번호 바꾸기) -int vaultIterationsForTest = VaultCrypto.defaultIterations; - class VaultPage extends StatefulWidget { final AppController c; @@ -48,7 +45,10 @@ class VaultPage extends StatefulWidget { /// [원본 지우기 (이동)] 일 때 탐색기의 지우기 (확인 창 · [permanent] 가 아니면 휴지통) final Future Function(List paths, {required bool permanent})? deleteOriginals; - const VaultPage({super.key, required this.c, this.add = const [], this.deleteOriginals}); + + /// 새 보관함 · 비밀번호 바꾸기의 PBKDF2 반복 수 + final int iterations; + const VaultPage({super.key, required this.c, this.add = const [], this.deleteOriginals, this.iterations = VaultCrypto.defaultIterations}); @override State createState() => _VaultPageState(); @@ -146,7 +146,7 @@ class _VaultPageState extends State with WidgetsBindingObserver { _stage = _Stage.opening; }); try { - final v = await Vault.create(_dir!, _pw.text, iterations: vaultIterationsForTest); + final v = await Vault.create(_dir!, _pw.text, iterations: widget.iterations); _pw.clear(); _pw2.clear(); if (!mounted) return; @@ -290,7 +290,7 @@ class _VaultPageState extends State with WidgetsBindingObserver { tooltip: tr('더 보기'), onSelected: (w) => switch (w) { 'mkdir' => vaultMakeFolder(context, _s), - 'pw' => vaultChangePassword(context, _s, iterations: vaultIterationsForTest), + 'pw' => vaultChangePassword(context, _s, iterations: widget.iterations), 'master' => vaultToggleMasterCopy(context, c, _s, !c.settings.vault.masterCopy), _ => null, }, diff --git a/test/remote_thumbs_test.dart b/test/remote_thumbs_test.dart index c1fc268..6366e18 100644 --- a/test/remote_thumbs_test.dart +++ b/test/remote_thumbs_test.dart @@ -129,22 +129,22 @@ void main() { final was = activeMeteredOf; tearDown(() { activeMeteredOf = was; - resetRemoteThumbNetForTest(); + forgetRemoteThumbNet(); }); AppController ctl() => AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); test('Wi-Fi 에서만 (처음 값): 데이터 요금제 망이면 아니오 · 아니면 예 · 끔 · 늘', () async { final c = ctl(); activeMeteredOf = () async => true; - resetRemoteThumbNetForTest(); + forgetRemoteThumbNet(); expect(await remoteThumbsAllowedNow(c), isFalse, reason: '모바일 데이터 (VPN 이어도 그 아래가 모바일 데이터)'); activeMeteredOf = () async => false; - resetRemoteThumbNetForTest(); + forgetRemoteThumbNet(); expect(await remoteThumbsAllowedNow(c), isTrue, reason: 'Wi-Fi (집 Wi-Fi 위의 Tailscale 포함)'); c.settings.explorer.remoteThumbs = 'off'; expect(await remoteThumbsAllowedNow(c), isFalse); c.settings.explorer.remoteThumbs = 'always'; activeMeteredOf = () async => true; - resetRemoteThumbNetForTest(); + forgetRemoteThumbNet(); expect(await remoteThumbsAllowedNow(c), isTrue); }); }); @@ -187,8 +187,8 @@ void main() { }); test('28 지적 (10/21): Range 를 모르는 서버 (200) 의 동영상은 썸네일을 받지 않음 · 서버마다 기억 · 206 서버는 만듦 · 시스템 폴더는 안 함', () async { - resetRemoteThumbRangeForTest(); - addTearDown(resetRemoteThumbRangeForTest); + forgetRemoteRangeSupport(); + addTearDown(forgetRemoteRangeSupport); server.ranges = false; for (final n in ['v.mp4', 'w.mp4']) { File(p.join(tmp.path, 'remote', n)).writeAsBytesSync(List.filled(1000, 7)); @@ -197,8 +197,8 @@ void main() { final calls = >[]; final c = AppController(PlatformServices(mediaTool: _Ffmpeg(calls), storage: DesktopStorageService())); c.settings.explorer.remoteThumbs = 'always'; - resetRemoteThumbsForTest(); - addTearDown(resetRemoteThumbsForTest); + forgetRemoteThumbHub(); + addTearDown(forgetRemoteThumbHub); final h = await remoteThumbsOf(c); final probes = server.rangeHeaders.length; expect(await h.request('dav://nas/v.mp4', 'k1', wanted: () => true), isNull); @@ -209,7 +209,7 @@ void main() { expect(await remoteVideoThumbOk('shz://sys/storage/emulated/0/a.mp4'), isFalse, reason: '시스템 폴더는 늘 통째 복사'); expect(await remoteVideoThumbOk('sftp://s/a.mp4'), isTrue); // 206 서버 (앱을 다시 켠 것처럼 기억을 지우고) - resetRemoteThumbRangeForTest(); + forgetRemoteRangeSupport(); server.ranges = true; expect(await remoteVideoThumbOk('dav://nas/v.bin'), isTrue); expect(await h.request('dav://nas/v.mp4', 'k3', wanted: () => true), isNull, reason: '가짜 FFmpeg 는 그림을 만들지 않음'); @@ -248,8 +248,8 @@ void main() { server.ranges = true; final c = AppController(PlatformServices(mediaTool: tool, storage: DesktopStorageService())); c.settings.explorer.remoteThumbs = 'always'; - resetRemoteThumbsForTest(); - addTearDown(resetRemoteThumbsForTest); + forgetRemoteThumbHub(); + addTearDown(forgetRemoteThumbHub); final h = await remoteThumbsOf(c); final st = File(mp4).statSync(); final f = await h.request('dav://nas/clip.mp4', RemoteThumbs.keyOf('dav://nas/clip.mp4', st.size, st.modified), wanted: () => true); diff --git a/test/root_test.dart b/test/root_test.dart index ac35322..294f3d4 100644 --- a/test/root_test.dart +++ b/test/root_test.dart @@ -64,7 +64,7 @@ void main() { tmp = Directory.systemTemp.createTempSync('jj_root_'); su = _Su(); SuRoot.start = su.start; - SuRoot.resetForTest(); + SuRoot.forgetGrant(); ShizukuRemote.rootBackend = SuRoot(); ShizukuRemote.tempDir = () async => tmp.path; rootSupported = () => true; @@ -106,7 +106,7 @@ void main() { await expectLater(r.copyTo('/data/bad', p.join(tmp.path, 'bad')), throwsA(isA())); expect(File(p.join(tmp.path, 'bad')).existsSync(), isFalse); // 거절 - SuRoot.resetForTest(); + SuRoot.forgetGrant(); su.allow = false; expect(await r.request(), isFalse); // su 없음 diff --git a/test/upscale_job_test.dart b/test/upscale_job_test.dart index 06908d4..ce9bdb5 100644 --- a/test/upscale_job_test.dart +++ b/test/upscale_job_test.dart @@ -20,12 +20,13 @@ void main() { late Directory tmp; late String input; late Completer engine; + late AiUpscaler Function() upscaler; 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( + upscaler = () => AiUpscaler( device: const SdDevice('sd-cli', 'cpu', 'CPU'), modelPath: 'm.pth', modelId: 'esrgan-x4plus', @@ -37,7 +38,6 @@ void main() { ); }); tearDown(() { - debugUpscalerForTest = null; AiJobs.instance.done(''); // 남은 [보기] 정리 tmp.deleteSync(recursive: true); }); @@ -71,7 +71,7 @@ void main() { }); testWidgets('[이 장] 은 작업 목록에 오르고, [뒤에서 계속] 으로 화면을 떠나도 끝나며 [보기] 로 비교 · 저장', (tester) async { - final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); + final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService(), createUpscaler: upscaler)); var finished = false; await tester.pumpWidget(MaterialApp( home: Scaffold( diff --git a/test/vault_page_test.dart b/test/vault_page_test.dart index 2def42b..530560f 100644 --- a/test/vault_page_test.dart +++ b/test/vault_page_test.dart @@ -19,6 +19,9 @@ import 'package:path/path.dart' as p; /// X-plore 비교 5-2 암호 보관함 화면 (관리자 결정 10/21): 만들기 창의 알림 · 넣기 (복사 / 원본 지우기) · 잠금 · 틀린 비밀번호 · /// 꺼내기 · 지우기 · [마스터로도 열기] (마스터를 초기화하면 그 길만 없어짐) +/// 시험의 PBKDF2 반복 수 (빨리 - 실제 암호화 그대로) +const _fastIterations = 1000; + void main() { late Directory tmp; final was = vaultDirOf; @@ -28,7 +31,6 @@ void main() { vaultDirOf = () async => Directory(p.join(tmp.path, 'vault')); vaultPickFolder = () async => p.join(tmp.path, 'out'); Directory(p.join(tmp.path, 'out')).createSync(); - vaultIterationsForTest = 1000; }); tearDown(() async { vaultDirOf = was; @@ -104,7 +106,7 @@ void main() { final permanents = []; final c = ctl(); await t.pumpWidget(MaterialApp( - home: VaultPage(c: c, add: [src.path], deleteOriginals: (paths, {required permanent}) async { + home: VaultPage(iterations: _fastIterations, c: c, add: [src.path], deleteOriginals: (paths, {required permanent}) async { deleted.add(paths); permanents.add(permanent); }))); @@ -171,7 +173,7 @@ void main() { await t.runAsync(() => lock.setMaster('master')); final c = ctl(); // 먼저 만들어 둔다 - await t.pumpWidget(MaterialApp(home: VaultPage(c: c))); + await t.pumpWidget(MaterialApp(home: VaultPage(iterations: _fastIterations, c: c))); await until(t, () => find.text('새 보관함 만들기').evaluate().isNotEmpty); await t.enterText(find.byType(TextField).at(0), 'pw'); await t.enterText(find.byType(TextField).at(1), 'pw'); @@ -269,7 +271,7 @@ void main() { final f = File(p.join(tmp.path, '사라질.pdf'))..writeAsBytesSync(List.filled(100, 3)); final e = (await t.runAsync(() => v.add(f.path)))!; File(p.join(dir.path, '${e.blob}.jjv')).deleteSync(); - await t.pumpWidget(MaterialApp(home: VaultPage(c: ctl()))); + await t.pumpWidget(MaterialApp(home: VaultPage(iterations: _fastIterations, c: ctl()))); await until(t, () => find.widgetWithText(FilledButton, '열기').evaluate().isNotEmpty); await t.enterText(find.byType(TextField), 'pw'); await t.tap(find.widgetWithText(FilledButton, '열기')); @@ -304,7 +306,7 @@ void main() { final photo = File(p.join(tmp.path, 'photo.png'))..writeAsBytesSync(img.encodePng(img.Image(width: 20, height: 10))); final clip = File(p.join(tmp.path, 'clip.mp4'))..writeAsBytesSync(List.filled(500, 1)); final c = ctl()..settings.vault.autoLockSeconds = 0; - await t.pumpWidget(MaterialApp(home: VaultPage(c: c, add: [photo.path, clip.path]))); + await t.pumpWidget(MaterialApp(home: VaultPage(iterations: _fastIterations, c: c, add: [photo.path, clip.path]))); await until(t, () => find.text('새 보관함 만들기').evaluate().isNotEmpty); await t.enterText(find.byType(TextField).at(0), 'pw'); await t.enterText(find.byType(TextField).at(1), 'pw'); @@ -367,7 +369,7 @@ void main() { final c = ctl(); final deleted = >[]; await t.pumpWidget(MaterialApp( - home: VaultPage(c: c, add: [album.path, remotePath], deleteOriginals: (paths, {required permanent}) async => deleted.add(paths)))); + home: VaultPage(iterations: _fastIterations, c: c, add: [album.path, remotePath], deleteOriginals: (paths, {required permanent}) async => deleted.add(paths)))); await until(t, () => find.text('새 보관함 만들기').evaluate().isNotEmpty); await t.enterText(find.byType(TextField).at(0), 'pw'); await t.enterText(find.byType(TextField).at(1), 'pw');