197 lines
11 KiB
Dart
197 lines
11 KiB
Dart
import 'dart:async';
|
|
import 'dart:io';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:jj_mkvmaker/app/app_controller.dart';
|
|
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/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:path/path.dart' as p;
|
|
|
|
import 'support/rsync_scenes.dart';
|
|
|
|
/// 관리자 지시 (10/20): 실시간 동기화 (로컬 rsync 쌍) 를 28 의 장면 · 옵션으로 - 결과 트리 = 실제 rsync,
|
|
/// 지울지 묻는 목록 = rsync 가 지우는 것, 맞춘 뒤 "맞출 것" 이 남지 않음 (거른 것을 기다리는 것으로 세지 않음)
|
|
void main() {
|
|
TestWidgetsFlutterBinding.ensureInitialized();
|
|
test('실시간 동기화 (로컬 rsync 쌍) 결과 = 실제 rsync 결과 · 지울지 묻는 목록 = rsync 가 지우는 것 (28 의 장면 · 옵션 28개)', () async {
|
|
final rsync = sceneRsync;
|
|
if (rsync == null) return markTestSkipped('앱에 든 rsync 가 없음 (tool\\fetch_rsync.ps1)');
|
|
final tmp = Directory.systemTemp.createTempSync('jj_livescene_');
|
|
addTearDown(() => tmp.deleteSync(recursive: true));
|
|
final src = p.join(tmp.path, 'src'), want = p.join(tmp.path, 'rsync'), got = p.join(tmp.path, 'live');
|
|
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
|
// 앱에 든 rsync 를 그대로 (시험 환경에는 앱 폴더 (path_provider) 가 없어 '직접 지정' 으로)
|
|
c.settings
|
|
..rsyncSource = 'custom'
|
|
..rsyncPath = rsync;
|
|
final bad = <String>[];
|
|
for (final opts in sceneOptions) {
|
|
writeScene(src, want);
|
|
final w = await realRsyncTree(rsync, src, want, opts);
|
|
writeScene(src, got);
|
|
final before = treeOf(got);
|
|
final gone = [for (final k in before.keys) if (!w.containsKey(k)) k];
|
|
c.settings.rsyncOptions = opts;
|
|
final pair = LiveSyncPair(src, got, method: 'rsync', delete: optionsDelete(opts));
|
|
c.settings.liveSyncPairs = [pair];
|
|
final live = LiveSync(c);
|
|
final k = LiveSync.keyOf(pair);
|
|
final why = <String>[];
|
|
await live.syncNow(pair);
|
|
if (live.status[k]!.$2.contains('실패')) why.add('상태: ${live.status[k]!.$2}');
|
|
final asked = live.toDelete[k] ?? const <String>[];
|
|
for (final g in gone) {
|
|
if (!asked.any((a) => g == a || g.startsWith('$a/'))) why.add('rsync 는 지우는데 묻지 않음: $g');
|
|
}
|
|
for (final a in asked) {
|
|
if (!gone.contains(a)) why.add('rsync 는 지우지 않는데 물음: $a');
|
|
}
|
|
// 관리자 3a 조건 (10/20): --delete-excluded 때문에 지워지는 것 (거른 것 · 거른 것이 든 폴더) 은 확인 창에서 따로
|
|
final excl = live.toDeleteExcluded[k] ?? const <String>[];
|
|
final wantExcl = {
|
|
'-a --delete --exclude=*.bak --delete-excluded': {'onlydst'},
|
|
'-a --delete --exclude=*.log --delete-excluded': {'keep.log', 'old', 'old2'},
|
|
}[opts] ?? const <String>{};
|
|
if (!RsyncFilter.parse(opts).deleteExcluded && excl.isNotEmpty) why.add('--delete-excluded 가 없는데 따로 묶음: $excl');
|
|
if (excl.toSet().difference(asked.toSet()).isNotEmpty) why.add('따로 묶은 것이 지울 목록에 없음: $excl');
|
|
if (excl.toSet().length != wantExcl.length || !excl.toSet().containsAll(wantExcl)) why.add('따로 묶은 것 $excl ≠ $wantExcl');
|
|
if (asked.isNotEmpty) await live.decideDelete(c.settings.liveSyncPairs.single, delete: true);
|
|
why.addAll(treeDiff(w, treeOf(got)));
|
|
if ((live.pending[k] ?? const []).isNotEmpty) why.add('맞춘 뒤에도 맞출 것: ${live.pending[k]}');
|
|
live.dispose();
|
|
if (why.isNotEmpty) bad.add('$opts\n ${why.join('\n ')}');
|
|
}
|
|
expect(bad, isEmpty, reason: bad.join('\n'));
|
|
}, timeout: const Timeout(Duration(minutes: 5)));
|
|
|
|
testWidgets('관리자 3a 조건 (10/20): 지울지 묻는 창 - --delete-excluded 로 지워지는 거른 것은 따로 묶어 빨갛게', (tester) async {
|
|
final rsync = sceneRsync;
|
|
if (rsync == null) return markTestSkipped('앱에 든 rsync 가 없음');
|
|
final tmp = Directory.systemTemp.createTempSync('jj_liveexcl_');
|
|
addTearDown(() => tmp.deleteSync(recursive: true));
|
|
final src = p.join(tmp.path, 's'), dst = p.join(tmp.path, 'd');
|
|
for (final (dir, rel) in [(src, 'a.txt'), (dst, 'only.txt'), (dst, 'skip.tmp'), (dst, 'cache/x.tmp')]) {
|
|
File(p.join(dir, rel))
|
|
..createSync(recursive: true)
|
|
..writeAsStringSync(rel);
|
|
}
|
|
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
|
c.settings
|
|
..rsyncSource = 'custom'
|
|
..rsyncPath = rsync
|
|
..rsyncOptions = '-av --exclude=*.tmp --delete-excluded';
|
|
final pair = LiveSyncPair(src, dst, method: 'rsync', delete: true);
|
|
c.settings.liveSyncPairs = [pair];
|
|
final live = LiveSync(c);
|
|
addTearDown(live.dispose);
|
|
await tester.runAsync(() => live.syncNow(pair));
|
|
final k = LiveSync.keyOf(pair);
|
|
expect(live.toDelete[k], unorderedEquals(['only.txt', 'skip.tmp', 'cache']));
|
|
expect(live.toDeleteExcluded[k], unorderedEquals(['skip.tmp', 'cache']), reason: 'cache 는 거른 것이 든 폴더');
|
|
expect(File(p.join(dst, 'skip.tmp')).existsSync(), isTrue, reason: '확인 전에는 지우지 않음');
|
|
late BuildContext ctx;
|
|
await tester.pumpWidget(MaterialApp(home: Builder(builder: (x) {
|
|
ctx = x;
|
|
return const SizedBox();
|
|
})));
|
|
unawaited(confirmLiveDeletes(ctx, live, pair));
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 300));
|
|
expect(find.text('대상에서 3개를 지울까요?'), findsOneWidget);
|
|
expect(find.text('거른 것 2개도 지워집니다 (--delete-excluded)'), findsOneWidget);
|
|
// 순서: 그냥 지울 것 → 묶음 제목 → 거른 것
|
|
final y = {for (final n in ['only.txt', '거른 것 2개도 지워집니다 (--delete-excluded)', 'skip.tmp', 'cache']) n: tester.getTopLeft(find.text(n)).dy};
|
|
expect(y['only.txt']! < y['거른 것 2개도 지워집니다 (--delete-excluded)']!, isTrue);
|
|
expect(y['거른 것 2개도 지워집니다 (--delete-excluded)']! < y['skip.tmp']!, isTrue);
|
|
expect(y['거른 것 2개도 지워집니다 (--delete-excluded)']! < y['cache']!, isTrue);
|
|
await tester.tap(find.text('나중에'));
|
|
await tester.pump();
|
|
});
|
|
|
|
test('관리자 3a 판단 (10/20): 확인한 쌍도 rsync 옵션이 바뀌어 지울 것이 늘면 다시 묻고, 늘지 않으면 묻지 않고 기억', () async {
|
|
final rsync = sceneRsync;
|
|
if (rsync == null) return markTestSkipped('앱에 든 rsync 가 없음');
|
|
final tmp = Directory.systemTemp.createTempSync('jj_reask_');
|
|
addTearDown(() => tmp.deleteSync(recursive: true));
|
|
final src = p.join(tmp.path, 's'), dst = p.join(tmp.path, 'd');
|
|
for (final (dir, rel) in [(src, 'a.txt'), (dst, 'only.tmp'), (dst, 'keep/x.tmp')]) {
|
|
File(p.join(dir, rel))
|
|
..createSync(recursive: true)
|
|
..writeAsStringSync(rel);
|
|
}
|
|
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
|
c.settings
|
|
..rsyncSource = 'custom'
|
|
..rsyncPath = rsync
|
|
..rsyncOptions = '-av --exclude=*.tmp';
|
|
// 이미 이 옵션으로 확인한 쌍
|
|
c.settings.liveSyncPairs = [
|
|
LiveSyncPair(src, dst, method: 'rsync', delete: true, deleteConfirmed: true, confirmedOptions: '-av --exclude=*.tmp'),
|
|
];
|
|
final live = LiveSync(c);
|
|
addTearDown(live.dispose);
|
|
LiveSyncPair pair() => c.settings.liveSyncPairs.single;
|
|
final k = LiveSync.keyOf(pair());
|
|
await live.syncNow(pair());
|
|
expect(live.toDelete[k], isNull);
|
|
expect(File(p.join(dst, 'a.txt')).existsSync(), isTrue);
|
|
|
|
// ① 지울 것이 늘지 않는 바꿈 (-h 더하기) → 묻지 않고 지금 옵션을 기억
|
|
c.settings.rsyncOptions = '-avh --exclude=*.tmp';
|
|
await live.syncNow(pair());
|
|
expect(live.toDelete[k], isNull);
|
|
expect(pair().confirmedOptions, '-avh --exclude=*.tmp');
|
|
|
|
// ② --delete-excluded 를 더함 → 거른 것이 새로 지워지므로 다시 묻고, 확인 전에는 지우지 않음
|
|
c.settings.rsyncOptions = '-avh --exclude=*.tmp --delete-excluded';
|
|
await live.syncNow(pair());
|
|
expect(live.toDelete[k], unorderedEquals(['only.tmp', 'keep']));
|
|
expect(live.reasked, contains(k));
|
|
expect(live.status[k]!.$2, contains('확인 필요'));
|
|
expect(File(p.join(dst, 'only.tmp')).existsSync(), isTrue, reason: '확인 전에는 지우지 않음');
|
|
expect(pair().deleteConfirmed, isTrue, reason: '쌍의 확인은 그대로 - 바뀐 옵션만 다시 묻는다');
|
|
// 확인하면 지우고 그 옵션을 기억
|
|
await live.decideDelete(pair(), delete: true);
|
|
expect(File(p.join(dst, 'only.tmp')).existsSync(), isFalse);
|
|
expect(Directory(p.join(dst, 'keep')).existsSync(), isFalse);
|
|
expect(pair().confirmedOptions, '-avh --exclude=*.tmp --delete-excluded');
|
|
expect(live.reasked, isNot(contains(k)));
|
|
|
|
// ③ 거르기를 빼면 (--exclude 를 지움) 지울 것은 줄거나 같다 → 묻지 않음
|
|
c.settings.rsyncOptions = '-avh';
|
|
await live.syncNow(pair());
|
|
expect(live.toDelete[k], isNull);
|
|
expect(pair().confirmedOptions, '-avh');
|
|
|
|
// ④ 예전에 확인한 쌍 (기억한 옵션 없음): --delete-excluded 로 새로 지워질 것이 있으면 묻는다 · 없으면 기억만
|
|
File(p.join(dst, 'new.tmp')).writeAsStringSync('n');
|
|
c.settings
|
|
..rsyncOptions = '-av --exclude=*.tmp --delete-excluded'
|
|
..liveSyncPairs = [LiveSyncPair.fromJson({'source': src, 'target': dst, 'method': 'rsync', 'delete': true})];
|
|
expect(pair().confirmedOptions, isNull);
|
|
await live.syncNow(pair());
|
|
expect(live.toDelete[k], ['new.tmp']);
|
|
expect(File(p.join(dst, 'new.tmp')).existsSync(), isTrue);
|
|
c.settings
|
|
..rsyncOptions = '-av --exclude=*.tmp'
|
|
..liveSyncPairs = [LiveSyncPair.fromJson({'source': src, 'target': dst, 'method': 'rsync', 'delete': true})];
|
|
await live.syncNow(pair());
|
|
expect(live.toDelete[k], isNull);
|
|
expect(pair().confirmedOptions, '-av --exclude=*.tmp');
|
|
expect(File(p.join(dst, 'new.tmp')).existsSync(), isTrue, reason: '거른 것 - 지우지 않음');
|
|
|
|
// 설정 파일: 기억한 옵션이 저장 · 읽기에서 남는다
|
|
final j = pair().toJson();
|
|
expect(j['confirmedOptions'], '-av --exclude=*.tmp');
|
|
expect(LiveSyncPair.fromJson(j).confirmedOptions, '-av --exclude=*.tmp');
|
|
expect(LiveSyncPair.fromJson({...j, 'confirmedOptions': 3}).confirmedOptions, isNull, reason: '틀린 값은 없는 것으로');
|
|
});
|
|
}
|