P0 실시간 동기화: 미리 볼 수 없는 거르기 규칙이 있는 지우기 쌍은 저절로 지우지 않고 보내기만 · 지우기는 확인 뒤 한 번 (검토 대기)
관리자 3a 지시 (10/20, 28 재현 - 데이터 버그 수정): 로컬 rsync 쌍 · 지우기 포함 · 옵션 --filter="-,s a.txt" (또는 "H a.txt" · -f"H a.txt") 에서 묻는 목록은 [extra.txt, old.log] 인데 [지우기] 를 누르면 a.txt 도 지워졌다 (실제 rsync 는 보내는 쪽만 빼기 · 숨기기를 받는 쪽에서 지울 것으로 봄). "P *.txt" 는 반대로 지킨 것을 끝없이 물었다. - 지우기 포함 rsync 쌍의 옵션에 미리 볼 수 없는 규칙 (RsyncFilter.unsupported) 이 하나라도 있으면 자동 맞추기는 --delete 계열 없이 (보내기만) 하고 묻지도 않는다 (목록이 실제와 달라서). 맞출 것 · 대상에만 있는 것도 지우기 없이 센다. - 카드에 빨간 "이 옵션의 일부 거르기 규칙은 미리 볼 수 없어 자동으로 지우지 않습니다 (규칙)" · [지금 지우기 (확인)]. 그 창은 FilterLimitNote (빨간 "실제로 더 지워질 수 있습니다") 와 [알고 실행합니다] 체크가 있어야 [지우고 맞추기] - 이번 한 번만 지우고 다음 자동 맞추기는 다시 보내기만. - 묻는 창 (confirmLiveDeletes) 도 같은 창을 써서 규칙이 있으면 FilterLimitNote · 체크. - P (지키기) 는 따로 따르지 않고 위와 같이 다룸 (경고 · 저절로 지우지 않음 · 끝없이 묻지 않음) - 지킨 것을 목록에서 빼려면 P · R 과 다른 규칙의 순서 (받는 쪽 규칙) 를 맞춰야 해서 이번에는 하지 않음. 시험: live_sync_scene_test P0 - 세 가지 규칙 × 새 쌍 · 확인한 쌍을 실제 rsync 로 (먼저 실제 rsync 가 a.txt 를 지우는 것 확인 → 자동 두 번은 아무것도 지우지 않음 · 묻지 않음 · − 줄 없음 → 지금 지우기 한 번 → 다음 자동은 다시 보내기만), P 규칙 세 번, 미리 볼 수 있는 규칙만이면 예전처럼, 창 (빨간 줄 · 체크 전 버튼 꺼짐 · [지우지 않기] 없음). LiveSync 시험 157개 통과. 캡처: windows_lsync_p0_shot_test (카드 · 지금 지우기 창, ko · en · ja × 412 · 960, 넘침 0) → release\test\shots_lsync_p0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>lsync_p0
parent
2f4ad6c146
commit
aba095b342
|
|
@ -1036,6 +1036,7 @@
|
|||
"대상 폴더가 없습니다: {0}": "Target folder not found: {0}",
|
||||
"대상에만 있는 {0}개를 지울지 확인이 필요합니다 (확인 전에는 지우지 않음)": "Confirm whether to delete {0} items that exist only in the target (nothing is deleted until you confirm)",
|
||||
"대상에서 {0}개를 지울까요?": "Delete {0} items from the target?",
|
||||
"대상에서 지울까요?": "Delete from the target?",
|
||||
"대상으로 옮긴 파일을 원본에서 지웁니다 (이동)": "Files transferred to the target are deleted from the source (move)",
|
||||
"대상이 원본과 같거나 원본 안에 있습니다": "The target is the same as or inside the source",
|
||||
"대상이 원본과 같거나 원본 안에 있습니다 (맞출 때마다 원본이 바뀝니다). 다른 폴더를 고르세요.": "The target is the same as or inside the source (the source would change every sync). Choose another folder.",
|
||||
|
|
@ -1260,6 +1261,7 @@
|
|||
"묻기 (기본)": "Ask (default)",
|
||||
"물어보지 않기": "Never ask",
|
||||
"미리 보기": "Quick look",
|
||||
"미리 본 지울 것은 없습니다 - 위 규칙 때문에 실제로는 지워질 수 있습니다": "The preview shows nothing to delete - but because of the rules above, items may actually be deleted",
|
||||
"미리 볼 수 없습니다: {0}": "Cannot preview: {0}",
|
||||
"미리 볼 수 없음 (글자 코드가 UTF-8 이 아닙니다)": "Can't preview (the text isn't UTF-8)",
|
||||
"미리 볼 수 없음 (글자 코드를 알 수 없습니다)": "Can't preview (unknown text encoding)",
|
||||
|
|
@ -2047,6 +2049,7 @@
|
|||
"이 연결은 서명도 암호화도 되지 않습니다 ({0}). 같은 네트워크의 다른 사람이 파일 내용을 보거나 바꿀 수 있으니, NAS 설정에서 SMB 서명이나 SMB3 암호화를 켜는 것이 좋습니다": "This connection is neither signed nor encrypted ({0}). Others on the same network could read or change the files, so it's best to turn on SMB signing or SMB3 encryption in the NAS settings",
|
||||
"이 연결은 암호화되지 않습니다 ({0}). 같은 네트워크의 다른 사람이 파일 내용을 볼 수 있으니, NAS 설정에서 SMB3 암호화를 켜는 것이 좋습니다": "This connection isn't encrypted ({0}). Others on the same network could see file contents, so it's best to turn on SMB3 encryption in the NAS settings",
|
||||
"이 옵션의 일부 거르기 규칙은 미리 볼 수 없습니다 - 실제로 더 지워질 수 있습니다: {0}": "Some filter rules in these options can't be previewed - more may actually be deleted: {0}",
|
||||
"이 옵션의 일부 거르기 규칙은 미리 볼 수 없어 자동으로 지우지 않습니다": "Some filter rules in these options cannot be previewed, so nothing is deleted automatically",
|
||||
"이 위치 (네트워크 드라이브 · 네트워크 공유 · USB 메모리 등) 에는 휴지통이 없어 바로 지워집니다.": "This location (network drive, network share, USB memory etc.) has no Recycle Bin, so items are deleted right away.",
|
||||
"이 이동 버튼 지우기": "Delete this move button",
|
||||
"이 이동을 다시 할 때 이 옵션을 씁니다. 지금 새 옵션으로 실행할까요?": "These options will be used the next time this move runs. Run it now with the new options?",
|
||||
|
|
@ -2414,6 +2417,7 @@
|
|||
"지금 쓰는 버전: v{0} → 새 버전: v{1}": "Current version: v{0} → new version: v{1}",
|
||||
"지금 작업 취소: {0}": "Cancelled current job: {0}",
|
||||
"지금 작업만 (대기 {0}개는 계속)": "Current job only ({0} waiting continue)",
|
||||
"지금 지우기 (확인)": "Delete now (confirm)",
|
||||
"지금 짝 추가": "Add current pair",
|
||||
"지금 짝을 즐겨찾기에": "Add the current pair to favorites",
|
||||
"지금 폴더": "Current folder",
|
||||
|
|
@ -2428,6 +2432,7 @@
|
|||
"지난 실행이 정상적으로 끝나지 않았습니다 (마지막 확인 {0}:{1}). 작업 기록에 남겨 두었습니다.": "The last run did not end normally (last check {0}:{1}). It has been noted in the job log.",
|
||||
"지난 작업 기록": "Old job logs",
|
||||
"지문 확인하고 연결": "Check fingerprint and connect",
|
||||
"지우고 맞추기": "Delete and sync",
|
||||
"지우고 초기화": "Delete and reset",
|
||||
"지우기": "Delete",
|
||||
"지우기 포함": "with delete",
|
||||
|
|
|
|||
|
|
@ -1036,6 +1036,7 @@
|
|||
"대상 폴더가 없습니다: {0}": "対象フォルダーがありません: {0}",
|
||||
"대상에만 있는 {0}개를 지울지 확인이 필요합니다 (확인 전에는 지우지 않음)": "対象にだけある {0} 個を削除するか確認が必要です (確認するまで削除しません)",
|
||||
"대상에서 {0}개를 지울까요?": "対象から {0} 個を削除しますか?",
|
||||
"대상에서 지울까요?": "対象から削除しますか?",
|
||||
"대상으로 옮긴 파일을 원본에서 지웁니다 (이동)": "対象へ移したファイルを元から削除します (移動)",
|
||||
"대상이 원본과 같거나 원본 안에 있습니다": "対象が元と同じか、元の中にあります",
|
||||
"대상이 원본과 같거나 원본 안에 있습니다 (맞출 때마다 원본이 바뀝니다). 다른 폴더를 고르세요.": "対象が元と同じか元の中にあります (同期するたびに元が変わります)。別のフォルダーを選んでください。",
|
||||
|
|
@ -1260,6 +1261,7 @@
|
|||
"묻기 (기본)": "確認する (既定)",
|
||||
"물어보지 않기": "尋ねない",
|
||||
"미리 보기": "クイックプレビュー",
|
||||
"미리 본 지울 것은 없습니다 - 위 규칙 때문에 실제로는 지워질 수 있습니다": "プレビューでは削除するものはありません - ただし上のルールのため実際には削除されることがあります",
|
||||
"미리 볼 수 없습니다: {0}": "プレビューできません: {0}",
|
||||
"미리 볼 수 없음 (글자 코드가 UTF-8 이 아닙니다)": "プレビューできません (文字コードが UTF-8 ではありません)",
|
||||
"미리 볼 수 없음 (글자 코드를 알 수 없습니다)": "プレビューできません (文字コードが分かりません)",
|
||||
|
|
@ -2047,6 +2049,7 @@
|
|||
"이 연결은 서명도 암호화도 되지 않습니다 ({0}). 같은 네트워크의 다른 사람이 파일 내용을 보거나 바꿀 수 있으니, NAS 설정에서 SMB 서명이나 SMB3 암호화를 켜는 것이 좋습니다": "この接続は署名も暗号化もされません ({0})。同じネットワークの他の人がファイルの内容を見たり変えたりできるため、NAS の設定で SMB 署名か SMB3 暗号化をオンにすることをおすすめします",
|
||||
"이 연결은 암호화되지 않습니다 ({0}). 같은 네트워크의 다른 사람이 파일 내용을 볼 수 있으니, NAS 설정에서 SMB3 암호화를 켜는 것이 좋습니다": "この接続は暗号化されていません ({0})。同じネットワークの他の人がファイルの内容を見られるため、NAS の設定で SMB3 暗号化をオンにすることをお勧めします",
|
||||
"이 옵션의 일부 거르기 규칙은 미리 볼 수 없습니다 - 실제로 더 지워질 수 있습니다: {0}": "このオプションの一部のフィルター規則はプレビューできません - 実際にはさらに削除される場合があります: {0}",
|
||||
"이 옵션의 일부 거르기 규칙은 미리 볼 수 없어 자동으로 지우지 않습니다": "このオプションの一部のフィルタールールはプレビューできないため、自動では削除しません",
|
||||
"이 위치 (네트워크 드라이브 · 네트워크 공유 · USB 메모리 등) 에는 휴지통이 없어 바로 지워집니다.": "この場所 (ネットワークドライブ・ネットワーク共有・USB メモリなど) にはごみ箱がないため、すぐに削除されます。",
|
||||
"이 이동 버튼 지우기": "この移動ボタンを削除",
|
||||
"이 이동을 다시 할 때 이 옵션을 씁니다. 지금 새 옵션으로 실행할까요?": "この移動を次に行うときにこのオプションを使います。今すぐ新しいオプションで実行しますか?",
|
||||
|
|
@ -2414,6 +2417,7 @@
|
|||
"지금 쓰는 버전: v{0} → 새 버전: v{1}": "現在のバージョン: v{0} → 新しいバージョン: v{1}",
|
||||
"지금 작업 취소: {0}": "現在の作業をキャンセル: {0}",
|
||||
"지금 작업만 (대기 {0}개는 계속)": "現在の作業だけ (待機 {0} 件は続行)",
|
||||
"지금 지우기 (확인)": "今すぐ削除 (確認)",
|
||||
"지금 짝 추가": "今のペアを追加",
|
||||
"지금 짝을 즐겨찾기에": "今のペアをお気に入りに",
|
||||
"지금 폴더": "現在のフォルダー",
|
||||
|
|
@ -2428,6 +2432,7 @@
|
|||
"지난 실행이 정상적으로 끝나지 않았습니다 (마지막 확인 {0}:{1}). 작업 기록에 남겨 두었습니다.": "前回の実行が正常に終了しませんでした(最終確認 {0}:{1})。作業記録に残しました。",
|
||||
"지난 작업 기록": "過去の作業記録",
|
||||
"지문 확인하고 연결": "フィンガープリントを確認して接続",
|
||||
"지우고 맞추기": "削除して同期",
|
||||
"지우고 초기화": "削除して初期化",
|
||||
"지우기": "削除",
|
||||
"지우기 포함": "削除あり",
|
||||
|
|
|
|||
|
|
@ -1036,6 +1036,7 @@
|
|||
"대상 폴더가 없습니다: {0}": "目标文件夹不存在: {0}",
|
||||
"대상에만 있는 {0}개를 지울지 확인이 필요합니다 (확인 전에는 지우지 않음)": "需要确认是否删除仅在目标中的 {0} 个项目 (确认前不会删除)",
|
||||
"대상에서 {0}개를 지울까요?": "从目标中删除 {0} 个项目吗?",
|
||||
"대상에서 지울까요?": "要从目标中删除吗?",
|
||||
"대상으로 옮긴 파일을 원본에서 지웁니다 (이동)": "已传到目标的文件从源中删除 (移动)",
|
||||
"대상이 원본과 같거나 원본 안에 있습니다": "目标与源相同或位于源内",
|
||||
"대상이 원본과 같거나 원본 안에 있습니다 (맞출 때마다 원본이 바뀝니다). 다른 폴더를 고르세요.": "目标与源相同或位于源内 (每次同步都会改变源)。请选择其他文件夹。",
|
||||
|
|
@ -1260,6 +1261,7 @@
|
|||
"묻기 (기본)": "询问 (默认)",
|
||||
"물어보지 않기": "不询问",
|
||||
"미리 보기": "快速预览",
|
||||
"미리 본 지울 것은 없습니다 - 위 규칙 때문에 실제로는 지워질 수 있습니다": "预览中没有要删除的项目 - 但由于上面的规则,实际上可能会被删除",
|
||||
"미리 볼 수 없습니다: {0}": "无法预览: {0}",
|
||||
"미리 볼 수 없음 (글자 코드가 UTF-8 이 아닙니다)": "无法预览 (文本不是 UTF-8)",
|
||||
"미리 볼 수 없음 (글자 코드를 알 수 없습니다)": "无法预览 (未知的文本编码)",
|
||||
|
|
@ -2047,6 +2049,7 @@
|
|||
"이 연결은 서명도 암호화도 되지 않습니다 ({0}). 같은 네트워크의 다른 사람이 파일 내용을 보거나 바꿀 수 있으니, NAS 설정에서 SMB 서명이나 SMB3 암호화를 켜는 것이 좋습니다": "此连接既未签名也未加密 ({0})。同一网络中的其他人可能查看或篡改文件内容,建议在 NAS 设置中开启 SMB 签名或 SMB3 加密",
|
||||
"이 연결은 암호화되지 않습니다 ({0}). 같은 네트워크의 다른 사람이 파일 내용을 볼 수 있으니, NAS 설정에서 SMB3 암호화를 켜는 것이 좋습니다": "此连接未加密 ({0})。同一网络中的其他人可能看到文件内容,建议在 NAS 设置中开启 SMB3 加密",
|
||||
"이 옵션의 일부 거르기 규칙은 미리 볼 수 없습니다 - 실제로 더 지워질 수 있습니다: {0}": "这些选项中的部分过滤规则无法预览 - 实际可能会删除更多: {0}",
|
||||
"이 옵션의 일부 거르기 규칙은 미리 볼 수 없어 자동으로 지우지 않습니다": "这些选项中的部分过滤规则无法预览,因此不会自动删除",
|
||||
"이 위치 (네트워크 드라이브 · 네트워크 공유 · USB 메모리 등) 에는 휴지통이 없어 바로 지워집니다.": "此位置 (网络驱动器 · 网络共享 · U 盘等) 没有回收站,会直接删除。",
|
||||
"이 이동 버튼 지우기": "删除此移动按钮",
|
||||
"이 이동을 다시 할 때 이 옵션을 씁니다. 지금 새 옵션으로 실행할까요?": "下次执行此移动时将使用这些选项。现在用新选项执行吗?",
|
||||
|
|
@ -2414,6 +2417,7 @@
|
|||
"지금 쓰는 버전: v{0} → 새 버전: v{1}": "当前版本: v{0} → 新版本: v{1}",
|
||||
"지금 작업 취소: {0}": "已取消当前任务: {0}",
|
||||
"지금 작업만 (대기 {0}개는 계속)": "仅当前任务 (等待中的 {0} 个继续)",
|
||||
"지금 지우기 (확인)": "立即删除 (确认)",
|
||||
"지금 짝 추가": "添加当前一对",
|
||||
"지금 짝을 즐겨찾기에": "将当前一对加入收藏",
|
||||
"지금 폴더": "当前文件夹",
|
||||
|
|
@ -2428,6 +2432,7 @@
|
|||
"지난 실행이 정상적으로 끝나지 않았습니다 (마지막 확인 {0}:{1}). 작업 기록에 남겨 두었습니다.": "上次运行未正常结束(最后检查 {0}:{1})。已记录在任务记录中。",
|
||||
"지난 작업 기록": "旧任务记录",
|
||||
"지문 확인하고 연결": "确认指纹并连接",
|
||||
"지우고 맞추기": "删除并同步",
|
||||
"지우고 초기화": "删除并重置",
|
||||
"지우기": "删除",
|
||||
"지우기 포함": "含删除",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,95 @@
|
|||
import 'dart:io';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:jj_mkvmaker/app/app_controller.dart';
|
||||
import 'package:jj_mkvmaker/app/i18n_controller.dart';
|
||||
import 'package:jj_mkvmaker/app/live_sync.dart';
|
||||
import 'package:jj_mkvmaker/app/settings.dart';
|
||||
import 'package:jj_mkvmaker/l10n/tr.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/monitor_page.dart';
|
||||
import 'package:jj_mkvmaker/ui/theme.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import 'support/temp_dir.dart';
|
||||
|
||||
/// P0 (관리자 3a 10/20): 미리 볼 수 없는 거르기 규칙 ("H a.txt") 이 있는 지우기 포함 쌍 - 모니터링 카드의 빨간 줄
|
||||
/// "자동으로 지우지 않습니다" · [지금 지우기 (확인)] 창 (FilterLimitNote · 체크) 을 412 · 960, ko · en · ja 로 찍는다.
|
||||
/// 시험 폴더 안에서만 (창은 [나중에] - 지우지 않음).
|
||||
/// 실행: flutter test integration_test\windows_lsync_p0_shot_test.dart -d windows (JJ_SHOT_DIR 에 PNG)
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
final out = Platform.environment['JJ_SHOT_DIR'] ?? p.join(Directory.systemTemp.path, 'jj_shots_lsync_p0');
|
||||
final rsync = p.join(Directory.current.path, 'third_party', 'rsync', 'windows', 'rsync.exe');
|
||||
for (final lang in ['ko', 'en', 'ja']) {
|
||||
for (final (w, h) in [(412, 900), (960, 600)]) {
|
||||
testWidgets('미리 볼 수 없는 규칙의 쌍 $lang ${w}x$h', (t) async {
|
||||
if (!File(rsync).existsSync()) return markTestSkipped('앱에 든 rsync 가 없음');
|
||||
t.view.physicalSize = Size(w.toDouble(), h.toDouble());
|
||||
t.view.devicePixelRatio = 1;
|
||||
addTearDown(t.view.reset);
|
||||
await t.runAsync(() => i18n.apply(lang, save: false));
|
||||
addTearDown(() => i18n.apply('ko', save: false));
|
||||
final tmp = testTempDir('jj_lsyncp0_shot_');
|
||||
final src = p.join(tmp.path, 's'), dst = p.join(tmp.path, 'd');
|
||||
for (final (dir, rel) in [(src, 'a.txt'), (src, 'b.txt'), (dst, 'a.txt'), (dst, 'old.log'), (dst, 'extra.txt')]) {
|
||||
File(p.join(dir, rel))
|
||||
..createSync(recursive: true)
|
||||
..writeAsStringSync('$dir/$rel');
|
||||
}
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
c.settings
|
||||
..rsyncSource = 'custom'
|
||||
..rsyncPath = rsync
|
||||
..rsyncOptions = '-a --filter="H a.txt"'
|
||||
..liveSyncPairs = [LiveSyncPair(src, dst, method: 'rsync', delete: true, deleteConfirmed: true)];
|
||||
final live = LiveSync(c)..start();
|
||||
addTearDown(live.dispose);
|
||||
final pair = c.settings.liveSyncPairs.single;
|
||||
await t.runAsync(() => live.syncNow(pair));
|
||||
expect(live.noAutoDelete[LiveSync.keyOf(pair)], isNotEmpty);
|
||||
final shotKey = GlobalKey();
|
||||
await t.pumpWidget(MaterialApp(
|
||||
theme: buildTheme(),
|
||||
debugShowCheckedModeBanner: false,
|
||||
builder: (_, child) => RepaintBoundary(key: shotKey, child: child),
|
||||
home: MonitorPage(c: c),
|
||||
));
|
||||
Future<void> settle([int n = 8]) async {
|
||||
for (var i = 0; i < n; i++) {
|
||||
await t.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 80)));
|
||||
await t.pump(const Duration(milliseconds: 80));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> shot(String name) async {
|
||||
await settle(3);
|
||||
expect(t.takeException(), isNull, reason: '$name 넘침');
|
||||
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));
|
||||
Directory(out).createSync(recursive: true);
|
||||
File(p.join(out, 'lsync_p0_${name}_${lang}_$w.png')).writeAsBytesSync(bytes!.buffer.asUint8List());
|
||||
}
|
||||
|
||||
await t.tap(find.text(tr('lsync (실시간 동기화)')));
|
||||
await settle();
|
||||
expect(find.text(tr('이 옵션의 일부 거르기 규칙은 미리 볼 수 없어 자동으로 지우지 않습니다')), findsOneWidget);
|
||||
await shot('1_card');
|
||||
await t.tap(find.text(tr('지금 지우기 (확인)')));
|
||||
await settle(12);
|
||||
expect(find.textContaining(tr('알고 실행합니다 (지울 목록이 실제와 다를 수 있음)')), findsOneWidget);
|
||||
await shot('2_now');
|
||||
await t.tap(find.text(tr('나중에')));
|
||||
await settle();
|
||||
expect(File(p.join(dst, 'old.log')).existsSync() && File(p.join(dst, 'a.txt')).existsSync(), isTrue, reason: '찍기만 - 지우지 않음');
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -178,6 +178,34 @@ class LiveSync extends ChangeNotifier {
|
|||
/// 확인한 뒤 옵션이 바뀌어 다시 묻는 쌍 (확인 창에 그 이유를 보인다)
|
||||
final reasked = <String>{};
|
||||
|
||||
/// P0 (관리자 3a 10/20, 28 재현): 지우기 포함 rsync 쌍의 옵션에 미리 볼 수 없는 거르기 규칙 (-,s · H · P · merge · --files-from 등)
|
||||
/// 이 있으면 실제 rsync 가 지울 목록에 없던 것까지 지울 수 있다 ("-,s a.txt" · "H a.txt" 는 받는 쪽 a.txt 를 지움).
|
||||
/// 저절로 도는 맞추기는 지우지 않고 보내기만 하고, 지우기는 카드의 [지금 지우기 (확인)] 로 한 번씩만. 값은 그 규칙들
|
||||
final noAutoDelete = <String, List<String>>{};
|
||||
final _deleteOnce = <String>{};
|
||||
|
||||
/// 이 쌍의 지우기에 걸리는 미리 볼 수 없는 거르기 규칙 (지우기 포함 rsync 쌍만)
|
||||
List<String> unsupportedOf(LiveSyncPair x) => x.delete ? _filterOf(x).unsupported : const [];
|
||||
|
||||
/// 미리 볼 수 없는 규칙이 있는 쌍의 "맞출 것" 은 지우기 없이 센다 (저절로 지우지 않으므로 − 줄이 늘 남지 않게)
|
||||
LiveSyncPair _pendingPair(LiveSyncPair x) => unsupportedOf(x).isNotEmpty ? x.copyWith(delete: false) : x;
|
||||
|
||||
/// [지금 지우기] 창에 보일 지울 것 (미리 본 것 - 위 규칙 때문에 실제와 다를 수 있음) · 그 가운데 --delete-excluded 로 지워지는 것
|
||||
Future<(List<String>, List<String>)> previewDeletes(LiveSyncPair x) async {
|
||||
final excl = <String>[];
|
||||
final del = [
|
||||
for (final d in await diff(x.copyWith(delete: true), limit: 100000, filter: _filterOf(x), excluded: excl))
|
||||
if (d.startsWith('− ')) d.substring(2)
|
||||
];
|
||||
return (del, excl);
|
||||
}
|
||||
|
||||
/// 확인 창 (FilterLimitNote 와 체크) 을 거친 뒤 이번 한 번만 지우기 포함으로 맞춘다. 다음 자동 맞추기는 다시 보내기만
|
||||
Future<void> deleteNow(LiveSyncPair x) async {
|
||||
_deleteOnce.add(keyOf(x));
|
||||
await syncNow(x);
|
||||
}
|
||||
|
||||
/// 받는 쪽에만 있는 [path] (상대 경로 [r]) 가 지워지는 모양을 [out] 에 "− 경로" 로 (rsync · _mirrorV 와 같게):
|
||||
/// 거른 것은 지우지 않고 (--delete-excluded 면 지움), 안에 거른 것이 든 폴더는 남기고 그 안의 거르지 않은 것만.
|
||||
/// 10/20 (15, 28 의 장면으로 찾음): 예전에는 그런 폴더도 "− 폴더" 한 줄이라 지울지 묻는 창에 폴더가 통째로 보이고,
|
||||
|
|
@ -345,8 +373,8 @@ class LiveSync extends ChangeNotifier {
|
|||
problems[k] = SourceUnreadableException(x.source, cause: e);
|
||||
}
|
||||
try {
|
||||
pending[k] = await diff(x, filter: _filterOf(x));
|
||||
await _countOnlyTarget(x);
|
||||
pending[k] = await diff(_pendingPair(x), filter: _filterOf(x));
|
||||
await _countOnlyTarget(_pendingPair(x));
|
||||
} catch (_) {}
|
||||
notifyListeners();
|
||||
}
|
||||
|
|
@ -503,8 +531,16 @@ class LiveSync extends ChangeNotifier {
|
|||
final asked = x;
|
||||
try {
|
||||
// 42: 지우기를 아직 확인하지 않았으면, 지울 것이 있을 때 지우지 않고 맞추고 확인을 기다린다 (목록은 [toDelete])
|
||||
var ask = x.delete && !x.deleteConfirmed;
|
||||
if (x.delete && x.deleteConfirmed && x.confirmedOptions != _optionsOf(x)) {
|
||||
final once = _deleteOnce.remove(k);
|
||||
final limits = unsupportedOf(x);
|
||||
if (limits.isEmpty) {
|
||||
noAutoDelete.remove(k);
|
||||
} else {
|
||||
noAutoDelete[k] = limits;
|
||||
}
|
||||
// 미리 볼 수 없는 규칙이 있으면 묻지도 않는다 (묻는 목록이 실제와 달라 - 28: 묻는 목록 밖의 a.txt 가 지워짐 · P 는 지킨 것을 끝없이 물음)
|
||||
var ask = limits.isEmpty && x.delete && !x.deleteConfirmed;
|
||||
if (limits.isEmpty && x.delete && x.deleteConfirmed && x.confirmedOptions != _optionsOf(x)) {
|
||||
if (await _deletesGrew(x)) {
|
||||
ask = true;
|
||||
reasked.add(k);
|
||||
|
|
@ -534,6 +570,7 @@ class LiveSync extends ChangeNotifier {
|
|||
toDeleteExcluded.remove(k);
|
||||
reasked.remove(k);
|
||||
}
|
||||
if (limits.isNotEmpty && !once) x = x.copyWith(delete: false); // 보내기만
|
||||
// 지우기 포함이면 원본을 확실히 읽을 수 있을 때만 (rsync --delete · robocopy /PURGE 도). 못 읽으면 멈추고 알린다
|
||||
final allowEmpty = _allowEmpty.remove(k);
|
||||
if (x.delete) await ensureSourceForDelete(x.source, x.target, allowEmpty: allowEmpty);
|
||||
|
|
@ -553,8 +590,8 @@ class LiveSync extends ChangeNotifier {
|
|||
: (DateTime.now(), n < 0 ? tr('맞춤') : trf('{0}개 맞춤', [n]));
|
||||
problems.remove(k);
|
||||
emptyDeletes.remove(k);
|
||||
pending[k] = await diff(asked, filter: _filterOf(asked));
|
||||
await _countOnlyTarget(asked);
|
||||
pending[k] = await diff(_pendingPair(asked), filter: _filterOf(asked));
|
||||
await _countOnlyTarget(_pendingPair(asked));
|
||||
if (n != 0) c.note(trf('실시간 동기화 ({0}): {1} → {2}', [method.label, x.source, x.target]));
|
||||
} on SourceUnreadableException catch (e) {
|
||||
// 68 · 70: 대상은 건드리지 않고 멈춤. 카드에 빨갛게 · 작업 알림에 · 로그에
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import 'path_label.dart';
|
|||
import 'rsync_setup.dart';
|
||||
import 'schedule_editor.dart';
|
||||
import '../core/sync_preview.dart' show optionsRemoveSource;
|
||||
import 'sync_preview_view.dart' show confirmDeletingRun, effectiveRunText;
|
||||
import 'sync_preview_view.dart' show FilterLimitNote, confirmDeletingRun, effectiveRunText;
|
||||
import 'theme.dart';
|
||||
|
||||
/// 모니터링 (파일 탐색기 > 모니터링): 복사 · rsync 와 lsync (실시간 동기화) 를 따로 보여 준다.
|
||||
|
|
@ -80,61 +80,87 @@ class MonitorPage extends StatelessWidget {
|
|||
Future<void> confirmLiveDeletes(BuildContext context, LiveSync live, LiveSyncPair x) async {
|
||||
final list = live.toDelete[LiveSync.keyOf(x)] ?? const <String>[];
|
||||
if (list.isEmpty) return;
|
||||
// 관리자 3a 조건 (10/20): --delete-excluded 때문에 지워지는 것 (거른 것 · 거른 것이 든 폴더) 은 따로 묶어 알고 누르게
|
||||
final excl = (live.toDeleteExcluded[LiveSync.keyOf(x)] ?? const <String>[]).toSet();
|
||||
final plain = [for (final n in list) if (!excl.contains(n)) n];
|
||||
final filtered = [for (final n in list) if (excl.contains(n)) n];
|
||||
final pick = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: Text(trf('대상에서 {0}개를 지울까요?', [list.length])),
|
||||
content: SizedBox(
|
||||
width: 560,
|
||||
// 세로 폰에서도 목록 (특히 거른 것 묶음) 이 더 보이게 화면 높이를 따라 (360 ~ 520)
|
||||
height: (MediaQuery.sizeOf(ctx).height * 0.55).clamp(360.0, 520.0),
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
// 관리자 3a 판단 (10/20): 확인한 쌍인데 옵션이 바뀌어 지울 것이 늘어 다시 묻는 것
|
||||
if (live.reasked.contains(LiveSync.keyOf(x))) ...[
|
||||
Text(tr('지우기를 확인한 뒤 rsync 옵션이 바뀌어 지울 것이 늘었습니다. 다시 확인해 주세요.'),
|
||||
style: const TextStyle(fontWeight: FontWeight.w600, color: Colors.orangeAccent)),
|
||||
const SizedBox(height: 6),
|
||||
],
|
||||
Text(trf('"{0}" 에는 없고 대상 "{1}" 에만 있는 항목입니다. 지우기 포함으로 맞추면 대상에서 지워집니다 (되돌릴 수 없음).',
|
||||
[vDisplay(x.source), vDisplay(x.target)])),
|
||||
const SizedBox(height: 8),
|
||||
Expanded(
|
||||
child: ListView(children: [
|
||||
for (final n in plain.take(500)) Text(n, style: const TextStyle(fontSize: 12, fontFamily: 'Consolas')),
|
||||
if (plain.length > 500) Text(trf('… 외 {0}개', [plain.length - 500])),
|
||||
if (filtered.isNotEmpty) ...[
|
||||
if (plain.isNotEmpty) const SizedBox(height: 10),
|
||||
Text(trf('거른 것 {0}개도 지워집니다 (--delete-excluded)', [filtered.length]),
|
||||
style: const TextStyle(fontWeight: FontWeight.w600, color: JjColors.danger)),
|
||||
Text(tr('rsync 옵션에서 뺀 것 (--exclude 등) 이지만 --delete-excluded 가 있어 대상에서 지웁니다. 지키려면 옵션에서 --delete-excluded 를 빼세요.'),
|
||||
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
const SizedBox(height: 4),
|
||||
for (final n in filtered.take(500)) Text(n, style: const TextStyle(fontSize: 12, fontFamily: 'Consolas', color: JjColors.danger)),
|
||||
if (filtered.length > 500) Text(trf('… 외 {0}개', [filtered.length - 500])),
|
||||
],
|
||||
]),
|
||||
),
|
||||
]),
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(ctx), child: Text(tr('나중에'))),
|
||||
TextButton(onPressed: () => Navigator.pop(ctx, false), child: Text(tr('지우지 않기 (지우기 끄기)'))),
|
||||
FilledButton(
|
||||
style: FilledButton.styleFrom(backgroundColor: JjColors.danger),
|
||||
onPressed: () => Navigator.pop(ctx, true),
|
||||
child: Text(trf('{0}개 지우고 맞추기', [list.length])),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
final pick = await _liveDeleteDialog(context, live, x, list, live.toDeleteExcluded[LiveSync.keyOf(x)] ?? const [], now: false);
|
||||
if (pick == null) return;
|
||||
await live.decideDelete(x, delete: pick);
|
||||
}
|
||||
|
||||
/// P0 (관리자 3a 10/20): 미리 볼 수 없는 거르기 규칙이 있어 자동으로 지우지 않는 쌍의 [지금 지우기 (확인)].
|
||||
/// 빨간 FilterLimitNote 와 [알고 실행합니다] 체크를 거쳐 이번 한 번만 지운다 (다음 자동 맞추기는 다시 보내기만)
|
||||
Future<void> confirmLiveDeleteNow(BuildContext context, LiveSync live, LiveSyncPair x) async {
|
||||
final (list, excl) = await live.previewDeletes(x);
|
||||
if (!context.mounted) return;
|
||||
final pick = await _liveDeleteDialog(context, live, x, list, excl, now: true);
|
||||
if (pick == true) await live.deleteNow(x);
|
||||
}
|
||||
|
||||
Future<bool?> _liveDeleteDialog(BuildContext context, LiveSync live, LiveSyncPair x, List<String> list, List<String> exclIn,
|
||||
{required bool now}) {
|
||||
// 관리자 3a 조건 (10/20): --delete-excluded 때문에 지워지는 것 (거른 것 · 거른 것이 든 폴더) 은 따로 묶어 알고 누르게
|
||||
final excl = exclIn.toSet();
|
||||
final plain = [for (final n in list) if (!excl.contains(n)) n];
|
||||
final filtered = [for (final n in list) if (excl.contains(n)) n];
|
||||
// P0: 미리 볼 수 없는 규칙 - 목록이 실제와 다를 수 있음을 빨갛게, 체크해야 [지우고 맞추기]
|
||||
final limits = live.unsupportedOf(x);
|
||||
var ack = false;
|
||||
return showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => StatefulBuilder(
|
||||
builder: (ctx, set) => AlertDialog(
|
||||
title: Text(list.isEmpty ? tr('대상에서 지울까요?') : trf('대상에서 {0}개를 지울까요?', [list.length])),
|
||||
content: SizedBox(
|
||||
width: 560,
|
||||
// 세로 폰에서도 목록 (특히 거른 것 묶음) 이 더 보이게 화면 높이를 따라 (360 ~ 520)
|
||||
height: (MediaQuery.sizeOf(ctx).height * 0.55).clamp(360.0, 520.0),
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
if (limits.isNotEmpty) ...[
|
||||
FilterLimitNote(unsupported: limits, deletes: true, ack: ack, onAck: (v) => set(() => ack = v)),
|
||||
const SizedBox(height: 4),
|
||||
],
|
||||
// 관리자 3a 판단 (10/20): 확인한 쌍인데 옵션이 바뀌어 지울 것이 늘어 다시 묻는 것
|
||||
if (!now && live.reasked.contains(LiveSync.keyOf(x))) ...[
|
||||
Text(tr('지우기를 확인한 뒤 rsync 옵션이 바뀌어 지울 것이 늘었습니다. 다시 확인해 주세요.'),
|
||||
style: const TextStyle(fontWeight: FontWeight.w600, color: Colors.orangeAccent)),
|
||||
const SizedBox(height: 6),
|
||||
],
|
||||
Text(trf('"{0}" 에는 없고 대상 "{1}" 에만 있는 항목입니다. 지우기 포함으로 맞추면 대상에서 지워집니다 (되돌릴 수 없음).',
|
||||
[vDisplay(x.source), vDisplay(x.target)])),
|
||||
const SizedBox(height: 8),
|
||||
Expanded(
|
||||
child: ListView(children: [
|
||||
if (list.isEmpty)
|
||||
Text(tr('미리 본 지울 것은 없습니다 - 위 규칙 때문에 실제로는 지워질 수 있습니다'),
|
||||
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
for (final n in plain.take(500)) Text(n, style: const TextStyle(fontSize: 12, fontFamily: 'Consolas')),
|
||||
if (plain.length > 500) Text(trf('… 외 {0}개', [plain.length - 500])),
|
||||
if (filtered.isNotEmpty) ...[
|
||||
if (plain.isNotEmpty) const SizedBox(height: 10),
|
||||
Text(trf('거른 것 {0}개도 지워집니다 (--delete-excluded)', [filtered.length]),
|
||||
style: const TextStyle(fontWeight: FontWeight.w600, color: JjColors.danger)),
|
||||
Text(tr('rsync 옵션에서 뺀 것 (--exclude 등) 이지만 --delete-excluded 가 있어 대상에서 지웁니다. 지키려면 옵션에서 --delete-excluded 를 빼세요.'),
|
||||
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
const SizedBox(height: 4),
|
||||
for (final n in filtered.take(500)) Text(n, style: const TextStyle(fontSize: 12, fontFamily: 'Consolas', color: JjColors.danger)),
|
||||
if (filtered.length > 500) Text(trf('… 외 {0}개', [filtered.length - 500])),
|
||||
],
|
||||
]),
|
||||
),
|
||||
]),
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(ctx), child: Text(tr('나중에'))),
|
||||
if (!now) TextButton(onPressed: () => Navigator.pop(ctx, false), child: Text(tr('지우지 않기 (지우기 끄기)'))),
|
||||
FilledButton(
|
||||
style: FilledButton.styleFrom(backgroundColor: JjColors.danger),
|
||||
onPressed: limits.isNotEmpty && !ack ? null : () => Navigator.pop(ctx, true),
|
||||
child: Text(list.isEmpty ? tr('지우고 맞추기') : trf('{0}개 지우고 맞추기', [list.length])),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 70: 원본이 비어 멈춘 쌍 - 지워질 목록을 보여 주고 [그래도 맞추기] 로 확인받는다 (42 와 같은 창)
|
||||
Future<void> confirmEmptySourceSync(BuildContext context, LiveSync live, LiveSyncPair x) async {
|
||||
final list = live.emptyDeletes[LiveSync.keyOf(x)] ?? const <String>[];
|
||||
|
|
@ -1020,6 +1046,21 @@ class _LiveTabState extends State<_LiveTab> {
|
|||
),
|
||||
]),
|
||||
),
|
||||
// P0 (관리자 3a 10/20): 미리 볼 수 없는 거르기 규칙 → 저절로 지우지 않고 보내기만. 지우기는 확인을 거쳐 한 번씩
|
||||
if (live != null && live.noAutoDelete[k] != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 32, top: 4),
|
||||
child: Wrap(spacing: 8, crossAxisAlignment: WrapCrossAlignment.center, children: [
|
||||
const Icon(Icons.block, size: 16, color: Colors.redAccent),
|
||||
Text(tr('이 옵션의 일부 거르기 규칙은 미리 볼 수 없어 자동으로 지우지 않습니다'),
|
||||
style: const TextStyle(fontSize: 12, color: Colors.redAccent, fontWeight: FontWeight.w600)),
|
||||
Text('(${live.noAutoDelete[k]!.join(' · ')})', style: const TextStyle(fontSize: 12, color: Colors.redAccent)),
|
||||
OutlinedButton(
|
||||
onPressed: () => confirmLiveDeleteNow(context, live, x),
|
||||
child: Text(tr('지금 지우기 (확인)')),
|
||||
),
|
||||
]),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 32, top: 4),
|
||||
child: Wrap(spacing: 16, runSpacing: 6, crossAxisAlignment: WrapCrossAlignment.center, children: [
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import 'package:jj_mkvmaker/app/live_sync.dart';
|
|||
import 'package:jj_mkvmaker/app/settings.dart';
|
||||
import 'package:jj_mkvmaker/core/rsync_filter.dart';
|
||||
import 'package:jj_mkvmaker/core/sync_preview.dart';
|
||||
import 'package:jj_mkvmaker/core/sync_tools.dart' show splitOptions, toCygwinPath;
|
||||
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';
|
||||
|
|
@ -193,4 +194,138 @@ void main() {
|
|||
expect(LiveSyncPair.fromJson(j).confirmedOptions, '-av --exclude=*.tmp');
|
||||
expect(LiveSyncPair.fromJson({...j, 'confirmedOptions': 3}).confirmedOptions, isNull, reason: '틀린 값은 없는 것으로');
|
||||
});
|
||||
|
||||
// P0 (관리자 3a 10/20, 28 재현): 미리 볼 수 없는 거르기 규칙이 있는 지우기 포함 쌍
|
||||
group('P0: 미리 볼 수 없는 거르기 규칙 - 자동은 보내기만 · 지우기는 확인 뒤 한 번', () {
|
||||
late Directory tmp;
|
||||
late String src, dst;
|
||||
late AppController c;
|
||||
setUp(() {
|
||||
tmp = Directory.systemTemp.createTempSync('jj_p0_');
|
||||
src = p.join(tmp.path, 's');
|
||||
dst = p.join(tmp.path, 'd');
|
||||
for (final (dir, rel, s) in [
|
||||
(src, 'a.txt', 's:a'),
|
||||
(src, 'b.txt', 's:b'),
|
||||
(src, 'c.log', 's:c'),
|
||||
(dst, 'a.txt', 'd:a'),
|
||||
(dst, 'old.log', 'd:old'),
|
||||
(dst, 'extra.txt', 'd:extra'),
|
||||
]) {
|
||||
File(p.join(dir, rel))
|
||||
..createSync(recursive: true)
|
||||
..writeAsStringSync(s);
|
||||
}
|
||||
c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
c.settings
|
||||
..rsyncSource = 'custom'
|
||||
..rsyncPath = sceneRsync ?? '';
|
||||
});
|
||||
tearDown(() => tmp.deleteSync(recursive: true));
|
||||
bool there(String rel) => File(p.join(dst, rel)).existsSync();
|
||||
|
||||
for (final opts in ['-a --filter="-,s a.txt"', '-a --filter="H a.txt"', '-a -f"H a.txt"']) {
|
||||
for (final confirmed in [false, true]) {
|
||||
test('28 재현 $opts (${confirmed ? '확인한 쌍' : '새 쌍'}): 묻는 목록 밖의 a.txt 가 지워지지 않음', () async {
|
||||
final rsync = sceneRsync;
|
||||
if (rsync == null) return markTestSkipped('앱에 든 rsync 가 없음');
|
||||
// 먼저 실제 rsync 가 이 규칙 + --delete 로 받는 쪽 a.txt 를 지우는 것을 확인 (위험이 실제로 있음)
|
||||
final probe = p.join(tmp.path, 'probe');
|
||||
Directory(probe).createSync();
|
||||
for (final f in Directory(dst).listSync()) {
|
||||
File(f.path).copySync(p.join(probe, p.basename(f.path)));
|
||||
}
|
||||
final r = await Process.run(rsync, ['-8', '--delete', ...splitOptions(opts), '${toCygwinPath(src)}/', '${toCygwinPath(probe)}/']);
|
||||
expect(r.exitCode, 0, reason: '${r.stderr}');
|
||||
expect(File(p.join(probe, 'a.txt')).existsSync(), isFalse, reason: '실제 rsync 는 지움');
|
||||
|
||||
c.settings.rsyncOptions = opts;
|
||||
final pair = LiveSyncPair(src, dst, method: 'rsync', delete: true, deleteConfirmed: confirmed, confirmedOptions: confirmed ? opts : null);
|
||||
c.settings.liveSyncPairs = [pair];
|
||||
final live = LiveSync(c);
|
||||
addTearDown(live.dispose);
|
||||
final k = LiveSync.keyOf(pair);
|
||||
for (var i = 0; i < 2; i++) {
|
||||
await live.syncNow(c.settings.liveSyncPairs.single);
|
||||
expect(live.status[k]!.$2, isNot(contains('실패')));
|
||||
expect([there('a.txt'), there('old.log'), there('extra.txt')], [true, true, true], reason: '자동 맞추기는 지우지 않음 ($i)');
|
||||
expect(there('b.txt') && there('c.log'), isTrue, reason: '보내기는 함');
|
||||
expect(live.noAutoDelete[k], isNotEmpty);
|
||||
expect(live.toDelete[k], isNull, reason: '묻지도 않음 (목록이 실제와 다름)');
|
||||
expect(live.pending[k]!.where((d) => d.startsWith('− ')), isEmpty, reason: '− 줄이 늘 남지 않음');
|
||||
}
|
||||
// [지금 지우기 (확인)] - 창에 보일 목록은 미리 본 것 (a.txt 는 없음 - 그래서 빨간 경고와 체크)
|
||||
final (list, _) = await live.previewDeletes(c.settings.liveSyncPairs.single);
|
||||
expect(list, unorderedEquals(['old.log', 'extra.txt']));
|
||||
await live.deleteNow(c.settings.liveSyncPairs.single);
|
||||
expect([there('old.log'), there('extra.txt')], [false, false]);
|
||||
// 다음 자동 맞추기는 다시 보내기만
|
||||
File(p.join(dst, 'extra2.txt')).writeAsStringSync('x');
|
||||
await live.syncNow(c.settings.liveSyncPairs.single);
|
||||
expect(there('extra2.txt'), isTrue);
|
||||
expect(live.noAutoDelete[k], isNotEmpty);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
test('P (지키기) 규칙: 경고만 · 지킨 것을 끝없이 묻지 않음 · 저절로 지우지 않음', () async {
|
||||
if (sceneRsync == null) return markTestSkipped('앱에 든 rsync 가 없음');
|
||||
c.settings.rsyncOptions = '-a --filter="P old.log"';
|
||||
final pair = LiveSyncPair(src, dst, method: 'rsync', delete: true);
|
||||
c.settings.liveSyncPairs = [pair];
|
||||
final live = LiveSync(c);
|
||||
addTearDown(live.dispose);
|
||||
final k = LiveSync.keyOf(pair);
|
||||
for (var i = 0; i < 3; i++) {
|
||||
await live.syncNow(c.settings.liveSyncPairs.single);
|
||||
expect(live.toDelete[k], isNull, reason: '묻지 않음 ($i)');
|
||||
expect(live.noAutoDelete[k], ['--filter=P old.log']);
|
||||
}
|
||||
expect(there('old.log') && there('extra.txt'), isTrue);
|
||||
});
|
||||
|
||||
test('미리 볼 수 있는 규칙만이면 예전처럼 (자동 지우기 · 묻기)', () async {
|
||||
if (sceneRsync == null) return markTestSkipped('앱에 든 rsync 가 없음');
|
||||
c.settings.rsyncOptions = '-a --exclude=*.log';
|
||||
final pair = LiveSyncPair(src, dst, method: 'rsync', delete: true);
|
||||
c.settings.liveSyncPairs = [pair];
|
||||
final live = LiveSync(c);
|
||||
addTearDown(live.dispose);
|
||||
await live.syncNow(pair);
|
||||
final k = LiveSync.keyOf(pair);
|
||||
expect(live.noAutoDelete[k], isNull);
|
||||
expect(live.toDelete[k], ['extra.txt']);
|
||||
});
|
||||
|
||||
testWidgets('[지금 지우기 (확인)] 창: 빨간 "미리 볼 수 없음" · 체크해야 [지우고 맞추기] · [지우지 않기] 는 없음', (tester) async {
|
||||
if (sceneRsync == null) return markTestSkipped('앱에 든 rsync 가 없음');
|
||||
c.settings.rsyncOptions = '-a --filter="H a.txt"';
|
||||
final pair = LiveSyncPair(src, dst, method: 'rsync', delete: true, deleteConfirmed: true);
|
||||
c.settings.liveSyncPairs = [pair];
|
||||
final live = LiveSync(c);
|
||||
addTearDown(live.dispose);
|
||||
late BuildContext ctx;
|
||||
await tester.pumpWidget(MaterialApp(home: Builder(builder: (x) {
|
||||
ctx = x;
|
||||
return const SizedBox();
|
||||
})));
|
||||
await tester.runAsync(() async {
|
||||
unawaited(confirmLiveDeleteNow(ctx, live, pair));
|
||||
await Future<void>.delayed(const Duration(milliseconds: 500));
|
||||
});
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
expect(find.textContaining('미리 볼 수 없습니다 - 실제로 더 지워질 수 있습니다'), findsOneWidget);
|
||||
expect(find.text('대상에서 2개를 지울까요?'), findsOneWidget);
|
||||
expect(find.text('지우지 않기 (지우기 끄기)'), findsNothing);
|
||||
FilledButton go() => tester.widget(find.widgetWithText(FilledButton, '2개 지우고 맞추기'));
|
||||
expect(go().onPressed, isNull, reason: '체크 전에는 꺼짐');
|
||||
await tester.tap(find.byType(Checkbox));
|
||||
await tester.pump();
|
||||
expect(go().onPressed, isNotNull);
|
||||
await tester.tap(find.text('나중에'));
|
||||
await tester.pump();
|
||||
expect(there('old.log') && there('extra.txt') && there('a.txt'), isTrue);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue