From e228e84adb01037c554ea55155c45277268f4869 Mon Sep 17 00:00:00 2001 From: RyuJongJin <266630912+RyuJongJin@users.noreply.github.com> Date: Tue, 20 Oct 2026 13:25:15 +0900 Subject: [PATCH] =?UTF-8?q?WIP=20=EC=8B=9C=ED=97=98=20=C2=B7=20=EC=95=B1?= =?UTF-8?q?=EC=9D=B4=20%TEMP%=20=EC=97=90=20=EA=B3=A0=EC=A0=95=20=EC=9D=B4?= =?UTF-8?q?=EB=A6=84=EC=9D=84=20=EB=82=A8=EA=B8=B0=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EA=B2=8C=20(=EA=B4=80=EB=A6=AC=EC=9E=90=20=EC=A7=80=EC=8B=9C?= =?UTF-8?q?=2010/20):=20=EC=8B=9C=ED=97=98=EC=9D=98=20=EC=95=B1=20?= =?UTF-8?q?=EC=9E=84=EC=8B=9C=20=ED=8F=B4=EB=8D=94=EB=8A=94=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=EB=A7=88=EB=8B=A4=20jj=5Fstore=5F=20(flutter=5Ftest?= =?UTF-8?q?=5Fconfig)=20=C2=B7=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20?= =?UTF-8?q?=EC=8B=9C=ED=97=98=EC=9D=80=20=EC=8B=9C=ED=97=98=20=ED=8F=B4?= =?UTF-8?q?=EB=8D=94=20(GitHubUpdater.tempRoot,=20=EA=B8=B0=EB=A1=9D?= =?UTF-8?q?=EC=9D=80=20=EC=8A=A4=ED=81=AC=EB=A6=BD=ED=8A=B8=20=EC=98=86)?= =?UTF-8?q?=20=C2=B7=20check.py=20=EB=8A=94=20=EC=9E=84=EC=8B=9C=20?= =?UTF-8?q?=ED=8F=B4=EB=8D=94=20=C2=B7=20=EC=95=B1=EC=9D=B4=20=EC=BC=9C?= =?UTF-8?q?=EC=A7=88=20=EB=95=8C=20=EC=A7=80=EB=82=9C=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8=EA=B0=80=20=EB=82=A8=EA=B8=B4=20=EC=83=88=20?= =?UTF-8?q?=ED=8C=90=20=ED=8F=B4=EB=8D=94=20=C2=B7=20.ps1=20=C2=B7=20.log?= =?UTF-8?q?=20=EB=A5=BC=20=EC=A7=80=EC=9B=80=20(2=EB=B6=84=20=EC=A7=80?= =?UTF-8?q?=EB=82=9C=20=EA=B2=83)=20=C2=B7=20=EA=B2=80=EC=82=AC=EB=8A=94?= =?UTF-8?q?=20jj=20=EB=A1=9C=20=EC=8B=9C=EC=9E=91=ED=95=98=EB=8A=94=20?= =?UTF-8?q?=EA=B2=83=20=EB=AA=A8=EB=91=90=20=EC=8B=A4=ED=8C=A8=20=C2=B7=20?= =?UTF-8?q?=EB=A7=88=EA=B7=B8=EB=84=B7=20=EB=A9=88=EC=B6=98=20=EB=A7=89?= =?UTF-8?q?=EB=8C=80=20"Stalled"=20=EB=94=B0=EB=A1=9C=20=EC=97=B4=EC=87=A0?= =?UTF-8?q?=20(28=20=EC=A7=80=EC=A0=81)=20=C2=B7=20explorer=5Ftest=2082=20?= =?UTF-8?q?=EB=B6=80=ED=95=98=20=EB=95=8C=20=EB=AA=A9=EB=A1=9D=20=EA=B8=B0?= =?UTF-8?q?=EB=8B=A4=EB=A6=AC=EA=B8=B0=20(=EA=B2=80=ED=86=A0=20=EB=8C=80?= =?UTF-8?q?=EA=B8=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- assets/l10n/en.json | 2 ++ assets/l10n/ja.json | 2 ++ assets/l10n/zh-Hans.json | 2 ++ .../windows_magnet_shot_test.dart | 2 +- lib/app/app_controller.dart | 5 ++++ lib/app/cleanup.dart | 23 ++++++++++++++++ .../windows/desktop_storage_service.dart | 6 ++++- lib/platform/windows/github_updater.dart | 13 +++++++--- lib/ui/downloads_page.dart | 3 ++- test/cleanup_test.dart | 26 +++++++++++++++++++ test/downloads_ui_test.dart | 2 +- test/explorer_test.dart | 2 ++ test/flutter_test_config.dart | 19 ++++++++++++++ test/update_test.dart | 9 ++++--- tool/l10n/check.py | 17 ++++++++---- tool/test_temp_check.ps1 | 22 ++++++---------- 16 files changed, 126 insertions(+), 29 deletions(-) create mode 100644 test/flutter_test_config.dart diff --git a/assets/l10n/en.json b/assets/l10n/en.json index d1d01b7..9a985d0 100644 --- a/assets/l10n/en.json +++ b/assets/l10n/en.json @@ -1196,6 +1196,7 @@ "먼저 mDNS 로 묻고 (이름을 알리는 NAS), 이어서 이 기기가 연결된 망 (Wi-Fi · 유선) 의 기기들에 SMB (445) 연결만 해 보고 바로 닫습니다. 파일 · 계정 정보는 보내지 않습니다.": "First asks via mDNS (NAS devices that announce their names), then tries an SMB (445) connection to devices on the network this device is on (Wi-Fi · wired) and closes it right away. No files or account details are sent.", "멈추고 옮기기": "Stop and move", "멈춤": "Stopped", + "멈춤 (시작 안 됨)": "Stalled", "멈춤 (이번 실행 동안)": "Pause (this session)", "멈춤 (이번 실행 동안) · 시작 버튼을 누르면 다시 동기화합니다": "Paused (this session) · press the start button to sync again", "멈춤 · {0}": "Stopped · {0}", @@ -2426,6 +2427,7 @@ "지금까지 찾은 {0}개 모두": "All {0} found so far", "지금은 듀얼 앱 (복제한 앱) 입니다. Android 가 듀얼 앱에는 이 권한을 주지 않을 수 있습니다. 허용 화면에서 켤 수 없으면 배지 없는 원래 JJ_MKVMaker 아이콘으로 여세요.": "This is the Dual App (cloned app). Android may not grant this permission to dual apps. If you can't turn it on, open the original JJ_MKVMaker icon without the badge.", "지난 실행이 정상적으로 끝나지 않았습니다 (마지막 확인 {0}:{1}). 작업 기록에 남겨 두었습니다.": "The last run did not end normally (last check {0}:{1}). It has been noted in the job log.", + "지난 업데이트가 남긴 임시 파일을 지움: {0}": "Removed temporary files left by the last update: {0}", "지난 작업 기록": "Old job logs", "지문 확인하고 연결": "Check fingerprint and connect", "지우고 초기화": "Delete and reset", diff --git a/assets/l10n/ja.json b/assets/l10n/ja.json index ea91f81..1cb7f31 100644 --- a/assets/l10n/ja.json +++ b/assets/l10n/ja.json @@ -1196,6 +1196,7 @@ "먼저 mDNS 로 묻고 (이름을 알리는 NAS), 이어서 이 기기가 연결된 망 (Wi-Fi · 유선) 의 기기들에 SMB (445) 연결만 해 보고 바로 닫습니다. 파일 · 계정 정보는 보내지 않습니다.": "まず mDNS で問い合わせ (名前を知らせる NAS)、続いてこの端末が接続しているネットワーク (Wi-Fi · 有線) の機器に SMB (445) で接続だけ試し、すぐに閉じます。ファイルやアカウント情報は送りません。", "멈추고 옮기기": "止めて移す", "멈춤": "停止", + "멈춤 (시작 안 됨)": "停滞 (開始されません)", "멈춤 (이번 실행 동안)": "停止 (今回の実行中)", "멈춤 (이번 실행 동안) · 시작 버튼을 누르면 다시 동기화합니다": "停止中 (今回の実行中) · 開始ボタンで再び同期します", "멈춤 · {0}": "停止 · {0}", @@ -2426,6 +2427,7 @@ "지금까지 찾은 {0}개 모두": "これまでに見つかった {0} 件すべて", "지금은 듀얼 앱 (복제한 앱) 입니다. Android 가 듀얼 앱에는 이 권한을 주지 않을 수 있습니다. 허용 화면에서 켤 수 없으면 배지 없는 원래 JJ_MKVMaker 아이콘으로 여세요.": "これはデュアルアプリ (複製したアプリ) です。Android はデュアルアプリにこの権限を与えない場合があります。許可画面でオンにできない場合は、バッジのない元の JJ_MKVMaker アイコンから開いてください。", "지난 실행이 정상적으로 끝나지 않았습니다 (마지막 확인 {0}:{1}). 작업 기록에 남겨 두었습니다.": "前回の実行が正常に終了しませんでした(最終確認 {0}:{1})。作業記録に残しました。", + "지난 업데이트가 남긴 임시 파일을 지움: {0}": "前回のアップデートが残した一時ファイルを削除: {0}", "지난 작업 기록": "過去の作業記録", "지문 확인하고 연결": "フィンガープリントを確認して接続", "지우고 초기화": "削除して初期化", diff --git a/assets/l10n/zh-Hans.json b/assets/l10n/zh-Hans.json index 2c26823..dbad801 100644 --- a/assets/l10n/zh-Hans.json +++ b/assets/l10n/zh-Hans.json @@ -1196,6 +1196,7 @@ "먼저 mDNS 로 묻고 (이름을 알리는 NAS), 이어서 이 기기가 연결된 망 (Wi-Fi · 유선) 의 기기들에 SMB (445) 연결만 해 보고 바로 닫습니다. 파일 · 계정 정보는 보내지 않습니다.": "先通过 mDNS 询问 (会公布名称的 NAS),再仅尝试与本设备所在网络 (Wi-Fi · 有线) 中的设备建立 SMB (445) 连接并立即关闭。不会发送文件或账户信息。", "멈추고 옮기기": "停止并移动", "멈춤": "已停止", + "멈춤 (시작 안 됨)": "停滞 (未开始)", "멈춤 (이번 실행 동안)": "暂停 (本次运行期间)", "멈춤 (이번 실행 동안) · 시작 버튼을 누르면 다시 동기화합니다": "已暂停 (本次运行期间) · 按开始按钮重新同步", "멈춤 · {0}": "已停止 · {0}", @@ -2426,6 +2427,7 @@ "지금까지 찾은 {0}개 모두": "目前找到的全部 {0} 个", "지금은 듀얼 앱 (복제한 앱) 입니다. Android 가 듀얼 앱에는 이 권한을 주지 않을 수 있습니다. 허용 화면에서 켤 수 없으면 배지 없는 원래 JJ_MKVMaker 아이콘으로 여세요.": "当前是双开应用 (复制的应用)。Android 可能不会给双开应用此权限。如果无法开启,请从没有徽标的原 JJ_MKVMaker 图标打开。", "지난 실행이 정상적으로 끝나지 않았습니다 (마지막 확인 {0}:{1}). 작업 기록에 남겨 두었습니다.": "上次运行未正常结束(最后检查 {0}:{1})。已记录在任务记录中。", + "지난 업데이트가 남긴 임시 파일을 지움: {0}": "已删除上次更新留下的临时文件: {0}", "지난 작업 기록": "旧任务记录", "지문 확인하고 연결": "确认指纹并连接", "지우고 초기화": "删除并重置", diff --git a/integration_test/windows_magnet_shot_test.dart b/integration_test/windows_magnet_shot_test.dart index ce11da5..8e22e26 100644 --- a/integration_test/windows_magnet_shot_test.dart +++ b/integration_test/windows_magnet_shot_test.dart @@ -80,7 +80,7 @@ void main() { expect(t.takeException(), isNull, reason: '넘침'); expect(find.textContaining('[시험] 아주 긴 이름'), findsOneWidget); expect(find.textContaining('Ubuntu 24.04.1 Desktop amd64'), findsOneWidget); - expect(find.text(tr('멈춤')), findsOneWidget); + expect(find.text(tr('멈춤 (시작 안 됨)')), findsOneWidget); final b = shotKey.currentContext!.findRenderObject()! as RenderRepaintBoundary; final img = await t.runAsync(() => b.toImage()); final bytes = await t.runAsync(() => img!.toByteData(format: ui.ImageByteFormat.png)); diff --git a/lib/app/app_controller.dart b/lib/app/app_controller.dart index a2b0d2d..906db8a 100644 --- a/lib/app/app_controller.dart +++ b/lib/app/app_controller.dart @@ -31,6 +31,7 @@ import '../services/model_store.dart'; import '../services/platform_services.dart'; import '../services/system_usage.dart'; import 'settings.dart'; +import 'cleanup.dart' show removeUpdateLeftovers; import '../l10n/tr.dart'; extension on Set { @@ -205,6 +206,10 @@ class AppController extends ChangeNotifier { ); _applySettings(); } + if (Platform.isWindows) { + final gone = removeUpdateLeftovers(Directory.systemTemp.path); + if (gone.isNotEmpty) _log(trf('지난 업데이트가 남긴 임시 파일을 지움: {0}', [gone.join(', ')])); + } ffmpegVersion = await _tool.version(); if (ffmpegVersion == null) { _log(tr('⚠ FFmpeg 를 찾을 수 없습니다. MKV 만들기를 사용할 수 없습니다.')); diff --git a/lib/app/cleanup.dart b/lib/app/cleanup.dart index 611c095..065706a 100644 --- a/lib/app/cleanup.dart +++ b/lib/app/cleanup.dart @@ -7,6 +7,29 @@ import '../l10n/tr.dart'; import 'app_controller.dart'; import 'download_manager.dart'; +/// 관리자 지시 (10/20): Windows 업데이트가 %TEMP% 에 남긴 것 - 풀어 둔 새 판 (jj_mkvmaker_update_<판>, 수백 MB) · +/// 교체 스크립트 (jj_mkvmaker_update.ps1) · 기록 (jj_mkvmaker_update.log). 예전에는 [정리] 를 누르기 전까지 남았다. +/// 앱이 켜질 때 부른다. 교체 스크립트가 아직 끝나는 중일 수 있어 [minAge] 보다 오래된 것만 지운다. 지운 이름을 돌려준다 +List removeUpdateLeftovers(String sysTmp, {Duration minAge = const Duration(minutes: 2), DateTime? now}) { + final mine = RegExp(r'^jj_mkvmaker_update(_.+|\.ps1|\.log)$'); + final before = (now ?? DateTime.now()).subtract(minAge); + final removed = []; + try { + for (final e in Directory(sysTmp).listSync(followLinks: false)) { + final name = p.basename(e.path); + if (!mine.hasMatch(name)) continue; + try { + if (!e.statSync().modified.isBefore(before)) continue; + e.deleteSync(recursive: true); + removed.add(name); + } catch (_) { + // 잡혀 있으면 (기록을 메모장으로 열어 둔 것 등) 다음에 + } + } + } catch (_) {} + return removed; +} + /// 정리할 파일 · 폴더 하나 class CleanupItem { final String path; diff --git a/lib/platform/windows/desktop_storage_service.dart b/lib/platform/windows/desktop_storage_service.dart index 6320663..103d3b2 100644 --- a/lib/platform/windows/desktop_storage_service.dart +++ b/lib/platform/windows/desktop_storage_service.dart @@ -9,6 +9,10 @@ import '../../services/storage_service.dart'; import '../../l10n/tr.dart'; class DesktopStorageService implements StorageService { + /// [tempDirectory] 의 기준 폴더 (null = 시스템 임시 폴더). 시험이 파일마다 따로 쓰고 끝에 지우게 한다 + /// (test/flutter_test_config.dart - 예전에는 시험이 %TEMP%\jj_mkvmaker 를 남겼다, 관리자 지시 10/20) + static String? tempRoot; + @override Future> pickVideos() async { final files = await FilePicker.pickFiles( @@ -108,7 +112,7 @@ class DesktopStorageService implements StorageService { @override Future tempDirectory() async { - final dir = Directory(p.join(Directory.systemTemp.path, 'jj_mkvmaker')); + final dir = Directory(p.join(tempRoot ?? Directory.systemTemp.path, 'jj_mkvmaker')); await dir.create(recursive: true); return dir.path; } diff --git a/lib/platform/windows/github_updater.dart b/lib/platform/windows/github_updater.dart index 0e34c92..b27d669 100644 --- a/lib/platform/windows/github_updater.dart +++ b/lib/platform/windows/github_updater.dart @@ -23,12 +23,19 @@ class GitHubUpdater implements Updater { ..userAgent = 'JJ_MKVMaker-updater' ..connectionTimeout = const Duration(seconds: 20); + /// 받은 업데이트 · 교체 스크립트 · 기록을 둘 곳 (null = 시스템 임시 폴더). 시험은 시험 폴더를 준다 + /// (예전에는 시험이 %TEMP% 에 jj_mkvmaker_update_1.9.0 · .ps1 · .log 를 남겼다, 관리자 지시 10/20) + final String? tempRoot; + GitHubUpdater({ this.apiBase = 'https://api.github.com', String? appDir, this.versionOverride, + this.tempRoot, }) : appDir = appDir ?? AppPaths.root; // Lib 구조면 배포 폴더 맨 위 + String get _temp => tempRoot ?? Directory.systemTemp.path; + @override bool get installsInPlace => false; @@ -81,7 +88,7 @@ class GitHubUpdater implements Updater { Future download(ReleaseInfo r, void Function(double progress) onProgress) async { final url = r.zipUrl; if (url == null) throw UpdateException(tr('이 버전에는 Windows 용 zip 이 없습니다.')); - final work = Directory(p.join(Directory.systemTemp.path, 'jj_mkvmaker_update_${r.version}')); + final work = Directory(p.join(_temp, 'jj_mkvmaker_update_${r.version}')); if (await work.exists()) await work.delete(recursive: true); await work.create(recursive: true); final zip = File(p.join(work.path, r.zipName ?? 'update.zip')); @@ -134,7 +141,7 @@ param([int]$ParentPid, [string]$Source, [string]$Target, [string]$Exe, [switch]$ $ErrorActionPreference = 'Continue' try { Wait-Process -Id $ParentPid -Timeout 90 -ErrorAction Stop } catch {} Start-Sleep -Milliseconds 800 -$log = Join-Path $env:TEMP 'jj_mkvmaker_update.log' +$log = Join-Path (Split-Path -Parent $PSCommandPath) 'jj_mkvmaker_update.log' # keep downloads, models and user data $lib = Join-Path $Source 'Lib' if (Test-Path $lib) { @@ -154,7 +161,7 @@ exit 0 /// 스크립트 실행. [waitPid] 가 끝나기를 기다렸다가 교체한다. Future runInstallScript(String source, {required int waitPid, bool restart = true}) async { - final ps1 = File(p.join(Directory.systemTemp.path, 'jj_mkvmaker_update.ps1')); + final ps1 = File(p.join(_temp, 'jj_mkvmaker_update.ps1')); await ps1.writeAsString(script); return Process.start( 'powershell.exe', diff --git a/lib/ui/downloads_page.dart b/lib/ui/downloads_page.dart index 218ce28..b25bd4e 100644 --- a/lib/ui/downloads_page.dart +++ b/lib/ui/downloads_page.dart @@ -419,7 +419,8 @@ class DownloadProgressBar extends StatelessWidget { : t.state == DownloadState.queued || t.extra['waiting'] == true ? tr('대기 중') : stalled - ? tr('멈춤') + // 관리자 지시 (10/20, 28 지적): 영어 "Stopped" 는 일시 정지 · 취소로 읽혀 따로 열쇠 ("Stalled") + ? tr('멈춤 (시작 안 됨)') : tr('준비 중…')) : '${(value * 100).toStringAsFixed(1)}%${size.isEmpty ? '' : ' · $size'}'; return ClipRRect( diff --git a/test/cleanup_test.dart b/test/cleanup_test.dart index c467fff..aabe8d7 100644 --- a/test/cleanup_test.dart +++ b/test/cleanup_test.dart @@ -143,4 +143,30 @@ void main() { expect(Cleaner.sizeText(5 * 1024 * 1024), '5.0 MB'); expect(Cleaner.sizeText(3 * 1024 * 1024 * 1024), '3.0 GB'); }); + test('관리자 지시 (10/20): 앱이 켜질 때 지난 업데이트가 남긴 것 (풀어 둔 새 판 · .ps1 · .log) 을 지운다 - 막 생긴 것 · 다른 것은 남김', () { + final tmp = Directory.systemTemp.createTempSync('jj_upd_left_'); + addTearDown(() => tmp.deleteSync(recursive: true)); + final old = DateTime.now().subtract(const Duration(hours: 1)); + void make(String rel, {bool fresh = false}) { + final f = File(p.join(tmp.path, rel))..createSync(recursive: true); + if (!fresh) f.setLastModifiedSync(old); + } + + make(p.join('jj_mkvmaker_update_2026.10.18_979', 'extract', 'jj_mkvmaker.exe')); + make('jj_mkvmaker_update.ps1'); + make('jj_mkvmaker_update.log', fresh: true); // 교체 스크립트가 막 쓴 것 + make('jj_mkvmaker_updater.txt'); + make(p.join('jj_mkvmaker', 'a.wav')); + // 폴더는 안에 파일을 만들어 바뀐 때가 지금이다 - 5분 뒤로 보고 지우는지 본다 + final dir = Directory(p.join(tmp.path, 'jj_mkvmaker_update_2026.10.18_979')); + final gone = removeUpdateLeftovers(tmp.path, now: DateTime.now().add(const Duration(minutes: 5))); + expect(gone..sort(), ['jj_mkvmaker_update.log', 'jj_mkvmaker_update.ps1', 'jj_mkvmaker_update_2026.10.18_979'], + reason: '5분 뒤로 보면 막 생긴 기록도 2분이 지남'); + expect(dir.existsSync(), isFalse); + expect(File(p.join(tmp.path, 'jj_mkvmaker_updater.txt')).existsSync(), isTrue); + expect(Directory(p.join(tmp.path, 'jj_mkvmaker')).existsSync(), isTrue); + + make('jj_mkvmaker_update.log', fresh: true); + expect(removeUpdateLeftovers(tmp.path), isEmpty, reason: '2분이 안 된 것은 남김 (스크립트가 끝나는 중일 수 있음)'); + }); } diff --git a/test/downloads_ui_test.dart b/test/downloads_ui_test.dart index e863a1b..f146f53 100644 --- a/test/downloads_ui_test.dart +++ b/test/downloads_ui_test.dart @@ -235,7 +235,7 @@ void main() { d.selectNone(); await tester.pump(); expect(find.textContaining('트래커 · 피어에 닿지 않습니다'), findsOneWidget); - expect(find.text('멈춤'), findsOneWidget); + expect(find.text('멈춤 (시작 안 됨)'), findsOneWidget); expect(find.text('준비 중…'), findsNothing); expect(bar().value, 0, reason: '움직이지 않는 막대'); expect(bar().color, Colors.orangeAccent.withValues(alpha: 0.85)); diff --git a/test/explorer_test.dart b/test/explorer_test.dart index f2cfec6..505f963 100644 --- a/test/explorer_test.dart +++ b/test/explorer_test.dart @@ -1429,6 +1429,8 @@ void main() { ..explorerDropAction = 'move' ..copyConflict = 'overwrite'; await open(tester); + // 전체 시험 중 (부하) 에는 목록을 늦게 읽어 e.txt 를 못 찾았다 - 두 파일이 보일 때까지 + await settle(tester, () => find.text('e.txt').evaluate().isNotEmpty && find.text('doc.txt').evaluate().isNotEmpty); await act(tester, () => tester.tap(find.text('선택').first)); await act(tester, () => tester.tap(markOf('doc.txt'))); await act(tester, () => tester.tap(markOf('e.txt'))); diff --git a/test/flutter_test_config.dart b/test/flutter_test_config.dart new file mode 100644 index 0000000..68efdd5 --- /dev/null +++ b/test/flutter_test_config.dart @@ -0,0 +1,19 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:jj_mkvmaker/platform/windows/desktop_storage_service.dart'; + +/// 모든 단위 시험 파일 앞에 돈다 (flutter_test 의 flutter_test_config). +/// 관리자 지시 (10/20): 앱 임시 폴더 ([DesktopStorageService.tempDirectory] = %TEMP%\jj_mkvmaker) 를 시험이 남겼다. +/// 시험 파일들은 나란히 돌아 같은 폴더를 끝에 지우면 서로 부딪치므로, 파일마다 따로 jj_store_… 를 주고 그 파일이 끝나면 지운다 +Future testExecutable(FutureOr Function() testMain) async { + final root = Directory.systemTemp.createTempSync('jj_store_'); + DesktopStorageService.tempRoot = root.path; + tearDownAll(() { + try { + root.deleteSync(recursive: true); + } catch (_) {} + }); + await testMain(); +} diff --git a/test/update_test.dart b/test/update_test.dart index a40893d..ee7c1d5 100644 --- a/test/update_test.dart +++ b/test/update_test.dart @@ -150,7 +150,7 @@ void main() { }); test('최신 확인 → 받기 · SHA256 확인 · 압축 풀기', () async { - final up = GitHubUpdater(apiBase: 'http://127.0.0.1:${server.port}', appDir: tmp.path, versionOverride: '1.0.1'); + final up = GitHubUpdater(apiBase: 'http://127.0.0.1:${server.port}', appDir: tmp.path, versionOverride: '1.0.1', tempRoot: tmp.path); final r = (await up.latest())!; expect(r.isNewerThan(await up.currentVersion()), isTrue); final progress = []; @@ -160,7 +160,7 @@ void main() { }); test('SHA256 이 다르면 설치 거부', () async { - final up = GitHubUpdater(apiBase: 'http://127.0.0.1:${server.port}', appDir: tmp.path); + final up = GitHubUpdater(apiBase: 'http://127.0.0.1:${server.port}', appDir: tmp.path, tempRoot: tmp.path); final good = (await up.latest())!; final bad = ReleaseInfo( version: good.version, tag: good.tag, name: '', notes: '', pageUrl: '', @@ -178,7 +178,7 @@ void main() { Directory(p.join(app.path, 'models')).createSync(); File(p.join(app.path, 'models', 'm.bin')).writeAsStringSync('model'); - final up = GitHubUpdater(apiBase: 'http://127.0.0.1:${server.port}', appDir: app.path); + final up = GitHubUpdater(apiBase: 'http://127.0.0.1:${server.port}', appDir: app.path, tempRoot: tmp.path); final dir = await up.download((await up.latest())!, (_) {}); // 이미 끝난 프로세스를 "앱" 으로 두고 실행 (바로 교체 진행) final dummy = await Process.start('cmd', ['/c', 'exit']); @@ -192,6 +192,9 @@ void main() { expect(File(p.join(app.path, 'old_only.txt')).readAsStringSync(), 'keep me'); expect(File(p.join(app.path, 'jj_yt-dlp', 'video.mp4')).readAsStringSync(), 'my download'); expect(File(p.join(app.path, 'models', 'm.bin')).readAsStringSync(), 'model'); + // 기록 · 스크립트는 스크립트 옆 (실제로는 %TEMP%, 시험은 시험 폴더 - 시험이 %TEMP% 에 남기지 않음) + expect(File(p.join(tmp.path, 'jj_mkvmaker_update.log')).existsSync(), isTrue); + expect(File(p.join(tmp.path, 'jj_mkvmaker_update.ps1')).existsSync(), isTrue); }, timeout: const Timeout(Duration(minutes: 2))); }); } diff --git a/tool/l10n/check.py b/tool/l10n/check.py index 9887682..fbe7929 100644 --- a/tool/l10n/check.py +++ b/tool/l10n/check.py @@ -8,15 +8,22 @@ """ import json import os +import shutil import subprocess import sys import tempfile here = os.path.dirname(os.path.abspath(__file__)) -out = os.path.join(tempfile.gettempdir(), 'jj_l10n_keys.json') -out2 = os.path.join(tempfile.gettempdir(), 'jj_l10n_unwrapped.json') -subprocess.run([sys.executable, os.path.join(here, 'extract_keys.py'), out, out2], check=True) -keys = json.load(open(out, encoding='utf-8')) +# 관리자 지시 (10/20): 예전에는 %TEMP% 에 jj_l10n_keys.json · jj_l10n_unwrapped.json 을 남겼다 - 임시 폴더를 만들고 끝에 지운다 +work = tempfile.mkdtemp(prefix='jj_l10n_check_') +try: + out = os.path.join(work, 'keys.json') + out2 = os.path.join(work, 'unwrapped.json') + subprocess.run([sys.executable, os.path.join(here, 'extract_keys.py'), out, out2], check=True) + keys = json.load(open(out, encoding='utf-8')) + unwrapped = json.load(open(out2, encoding='utf-8')) +finally: + shutil.rmtree(work, ignore_errors=True) missing = 0 for lang in ['en', 'ja', 'zh-Hans']: d = json.load(open(os.path.join('assets', 'l10n', lang + '.json'), encoding='utf-8')) @@ -26,7 +33,7 @@ for lang in ['en', 'ja', 'zh-Hans']: print('%s 사전에 없음: %r' % (lang, k)) # 60: tr 로 감싸지 않은 한글 글. 사전에 있으면 tr(변수) 로 번역되는 것으로 본다 - 없으면 번역이 빠진 것 en = json.load(open(os.path.join('assets', 'l10n', 'en.json'), encoding='utf-8')) -loose = [(p, n, t) for p, n, t in json.load(open(out2, encoding='utf-8')) if t not in en] +loose = [(p, n, t) for p, n, t in unwrapped if t not in en] for p, n, t in loose: print('번역 안 된 글 (tr 로 감싸거나 사전에 넣기, 자료면 // l10n-skip): %s:%d %r' % (p, n, t[:80])) missing += len(loose) diff --git a/tool/test_temp_check.ps1 b/tool/test_temp_check.ps1 index 9c5d360..cbb7224 100644 --- a/tool/test_temp_check.ps1 +++ b/tool/test_temp_check.ps1 @@ -5,8 +5,8 @@ # - 앱 코드 (lib) 가 시험 중에 만든 임시 폴더 (jj_up_ · jj_7z_ 등) 도 같은 곳에 생기므로 함께 잡힌다. # - 표는 환경 변수 JJ_TEST_TMP_TAG 로도 시험에 넘긴다: 시험 도우미 (test/support/temp_dirs.dart · integration_test/support/temp_dir.dart) # 가 임시 폴더가 그 안에 생기는지 확인한다 (통합 시험 -d windows 에서 앱까지 환경 변수가 가는지). -# - 끝나면 남은 것을 보이고 실행 폴더를 통째로 지운다. 실패 (exit 2) 는 시험 소스가 만드는 이름 (createTemp('jj_x_') 등) 의 폴더가 남았을 때만 -# (앱 코드의 고정 이름 jj_mkvmaker · 파일 · 앱이 끝날 때까지 잡힌 jj_held_ 는 참고로만). +# - 끝나면 남은 것을 보이고 실행 폴더를 통째로 지운다. jj 로 시작하는 것 (폴더 · 파일, 앱 고정 이름 포함) 이 남으면 실패 (exit 2) +# (앱이 끝날 때까지 잡힌 jj_held_ 와 jj 가 아닌 것은 참고로만). # 사용 (app 폴더에서): powershell -ExecutionPolicy Bypass -File tool\test_temp_check.ps1 [flutter test 인자…] # 통합 시험 · 캡처도 (관리자 지시 10/19, 28 이 찾음): … test_temp_check.ps1 integration_test\windows_b6_shot_test.dart -d windows $ErrorActionPreference = 'Stop' @@ -38,19 +38,13 @@ Start-Sleep -Seconds 3 $items = @(Get-ChildItem -LiteralPath $run -Force -ErrorAction SilentlyContinue) # jj_held_ 는 앱이 끝날 때까지 놓지 않는 폴더 (integration_test/support/temp_dir.dart 의 heldByApp - Edge 사용자 데이터) - 보이기만 하고 아래에서 함께 지운다 $held = @($items | Where-Object { $_.Name -like 'jj_held_*' } | ForEach-Object { $_.Name }) -# 실패로 세는 것: 시험 소스가 만드는 이름 (createTemp('jj_x_') · testTempDir('jj_x_') · tempDirUntilEnd('jj_x_')) 의 폴더 (예전 검사와 같은 기준) -$prefixes = @(@('test', 'integration_test') | ForEach-Object { Join-Path $app $_ } | Where-Object { Test-Path $_ } | - ForEach-Object { Get-ChildItem $_ -Recurse -Filter *.dart } | - ForEach-Object { [regex]::Matches((Get-Content $_.FullName -Raw -Encoding utf8), '(?:createTemp(?:Sync)?|testTempDir|tempDirUntilEnd)\(''(jj[^''$]*)''') } | - ForEach-Object { $_.Groups[1].Value } | Sort-Object -Unique) -$left = @($items | Where-Object { $_.PSIsContainer -and $_.Name -notlike 'jj_held_*' } | Where-Object { - $n = $_.Name - @($prefixes | Where-Object { $n.StartsWith($_) }).Count -gt 0 - } | ForEach-Object { $_.Name }) -# 참고: 그 밖의 것 (flutter 도구 · 앱 코드의 고정 이름 jj_mkvmaker 등 · 파일 - 실패로 세지 않고 보이기만, 실행 폴더와 함께 지움) -$other = @($items | Where-Object { $_.Name -notlike 'jj_held_*' -and $left -notcontains $_.Name } | ForEach-Object { $_.Name }) +# 실패로 세는 것: jj 로 시작하는 것 모두 (폴더 · 파일 - 시험 소스 이름 · 앱 코드의 고정 이름 jj_mkvmaker · jj_mkvmaker_update_* 등). +# 관리자 판단 (10/20): 앱 고정 이름도 사용자 PC 의 실제 %TEMP% 에 쌓이던 것이라 새면 잡는다 +$left = @($items | Where-Object { $_.Name -like 'jj*' -and $_.Name -notlike 'jj_held_*' } | ForEach-Object { $_.Name }) +# 참고: jj 가 아닌 것 (flutter 도구 · MSBuild 등 - 실패로 세지 않고 보이기만, 실행 폴더와 함께 지움) +$other = @($items | Where-Object { $_.Name -notlike 'jj*' } | ForEach-Object { $_.Name }) if ($other.Count -gt 0) { - Write-Host ("참고 - 시험 이름이 아닌 남은 것 {0}개 (실행 폴더와 함께 지움):" -f $other.Count) + Write-Host ("참고 - jj 가 아닌 남은 것 {0}개 (실행 폴더와 함께 지움):" -f $other.Count) $other | Sort-Object | ForEach-Object { Write-Host " $_" } } if ($held.Count -gt 0) {