JJ_MKVMaker/test/rsync_phsr_test.dart

194 lines
10 KiB
Dart

import 'dart:io';
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/file_ops.dart';
import 'package:jj_mkvmaker/core/rsync_filter.dart';
import 'package:jj_mkvmaker/core/sync_preview.dart';
import 'package:jj_mkvmaker/core/webdav.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:path/path.dart' as p;
import 'support/dav_server.dart';
import 'support/rsync_scenes.dart';
/// 관리자 지시 (10/20): H · S · P · R 과 고치개 ,s · ,r 을 rsync 와 똑같이 따른다 - 기준은 실제 rsync 를 돌린 (-n 아님) 결과 트리.
/// 장면 · 옵션마다 ① 미리 보기 (previewSync 로 예측한 트리) ② 원격 맞추기 (WebDAV 시험 서버) ③ 실시간 동기화 (로컬 rsync 쌍:
/// 묻는 목록 = rsync 가 지운 것 · 맞춘 뒤 맞출 것 없음) ④ 실시간 동기화 (WebDAV 쌍) 가 모두 실제 rsync 와 같아야 한다.
/// 하나라도 다르면 그 규칙은 미리 볼 수 없는 것 (unsupported) 으로 남겨야 한다.
/// 보내는 쪽 (양쪽에 같은 이름이 많게 - H · S 는 보내기, P · R 은 지우기에 걸리게)
const _src = [
'a.txt', 'b.log', 'keep.log', 'both.txt', 'both.log', 'shared/x.txt', 'shared/y.log', 'build/x.o', 'build/sub/y.o',
'sub/c.txt', 'sub/deep/d.txt', 'sub/deep/e.log', 'tmp/t.txt', '한글 폴더/가.txt', 'docs/r.md', 'z.txt', 'keep/k.txt',
];
/// 받는 쪽 (같은 이름은 크기가 달라 늘 바뀜 · 받는 쪽에만 있는 것은 지우기 대상)
const _dst = [
'a.txt', 'keep.log', 'both.txt', 'both.log', 'shared/x.txt', 'shared/onlyd.log', 'shared/onlyd.txt', 'build/old.o',
'old/keep.log', 'old/gone.txt', 'old2/deep/x/keep.log', 'old2/deep/x/g.txt', 'old2/e.txt', 'tmp/old.txt', 'extra.txt',
'extra.log', 'sub/deep/z.txt', '한글 폴더/나.txt', 'onlydst/a.bak',
// 28 확인 (10/20): 원본에도 있는 폴더 안의, 받는 쪽에만 있는 파일 · 하위 폴더 ("P keep/***" 가 안쪽까지 지키는지)
'keep/extra.txt', 'keep/sub/deep.txt',
];
/// 규칙 장면 (지우기 포함이 많게 - 지우기가 데이터 쪽 위험)
const phsrOptions = [
'-a --delete --filter="H *.log"',
'-a --delete --filter="-,s *.log"',
'-a --delete --filter="P *.log"',
'-a --delete --filter="-,r *.log"',
'-a --delete --filter="P old/"',
'-a --delete --filter="R *.log" --exclude=*.log',
'-a --delete --exclude=*.log --filter="R keep.log"',
'-a --delete --filter="R keep.log" --exclude=*.log',
'-a --delete --exclude=*.log --delete-excluded --filter="P keep.log"',
'-a --delete --filter="P keep.log" --exclude=*.log --delete-excluded',
'-a --delete --filter="S a.txt" --filter="H *.txt"',
'-a --delete --filter="H *.txt" --filter="S a.txt"',
'-a --delete --filter="H build/"',
'-a --delete --filter="P build/"',
'-a --delete --filter="-,sr *.log"',
'-a --delete --filter="-,sr *.log" --delete-excluded',
'-a --delete --filter="hide *.log"',
'-a --delete --filter="protect /old2/"',
'-a --delete --filter="risk *.txt" --filter="P *"',
'-a --delete --filter="P *" --filter="R *.txt"',
'-a --delete -f"H deep/" -f"P z.txt"',
'-a --delete --filter="H tmp/" --filter="P tmp/old.txt"',
'-a --delete --filter="+,r old/***" --exclude=old/',
'-a --delete --filter="H 한글*/"',
'-a --delete --filter="S *.log" --exclude=*.log',
'-a --delete --filter="+,s *.log" --exclude=*.log',
'-a --delete --filter="show shared/" --filter="hide shared/*"',
'-a --delete --filter="H *.log" --delete-excluded',
'-a --delete --filter="P shared/onlyd.*" --filter="H shared/"',
'-a --filter="H *.log"',
'-a --filter="P *.log" --exclude=*.txt',
// 28 확인 (10/20): "이름/***" 이 안쪽 (받는 쪽에만 있는 하위 폴더) 까지
'-a --delete --filter="R keep/extra.txt" --filter="P keep/***"',
'-a --delete --filter="P keep/***"',
'-a --delete --exclude=keep/***',
'-a --delete --filter="H keep/***"',
];
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final rsync = sceneRsync;
late Directory tmp, remote;
late TestDavServer server;
const id = 'nas';
DavServer dav() => DavServer(id: id, name: '시험', url: server.url, user: 'user', password: 'pass');
setUp(() async {
// 위젯 시험 바인딩은 HttpClient 를 가짜 (늘 400) 로 바꾼다 - 시험 WebDAV 서버 (127.0.0.1) 에 실제로 묻는다
HttpOverrides.global = null;
tmp = Directory.systemTemp.createTempSync('jj_phsr_');
remote = Directory(p.join(tmp.path, 'remote'))..createSync();
server = TestDavServer(remote);
await server.start();
DavRegistry.configure([dav()]);
});
tearDown(() async {
DavRegistry.configure([]);
await server.stop();
tmp.deleteSync(recursive: true);
});
test('이 규칙들은 이제 미리 볼 수 있다 (unsupported 없음) · 그 밖의 고치개 · merge 는 그대로 알림', () {
for (final o in phsrOptions) {
expect(RsyncFilter.parse(o).unsupported, isEmpty, reason: o);
}
for (final o in ['--filter="-,! a"', '--filter="-,p a"', '--filter="-,x a"', '--filter="H,r a"', '--filter=". rules"', '--filter="merge x"', '--filter="!"']) {
expect(RsyncFilter.parse(o).unsupported, isNotEmpty, reason: o);
}
// 쪽: H 는 보내기에만 (받는 쪽을 지키지 않음) · P 는 지우기에만 (보내기는 그대로)
final h = RsyncFilter.parse('--filter="H *.log"');
expect((h.excluded('a.log', isDir: false), h.protects('a.log', isDir: false)), (true, false));
final pr = RsyncFilter.parse('--filter="P *.log"');
expect((pr.excluded('a.log', isDir: false), pr.protects('a.log', isDir: false)), (false, true));
// --delete-excluded: 쪽을 적지 않은 규칙은 보내기에만 · ,sr 로 적은 것은 양쪽
final de = RsyncFilter.parse('--exclude=*.log --filter="-,sr *.bak" --delete-excluded');
expect((de.excluded('a.log', isDir: false), de.protects('a.log', isDir: false)), (true, false));
expect((de.excluded('a.bak', isDir: false), de.protects('a.bak', isDir: false)), (true, true));
});
for (final (i, opts) in phsrOptions.indexed) {
test('${i + 1}: $opts - 미리 보기 · 원격 맞추기 · 실시간 동기화 (로컬 · 원격) = 실제 rsync', () async {
if (rsync == null) return markTestSkipped('앱에 든 rsync 가 없음 (tool\\fetch_rsync.ps1)');
final src = p.join(tmp.path, 'src'), want = p.join(tmp.path, 'rsync');
writeScene(src, want, srcFiles: _src, dstFiles: _dst);
final before = treeOf(want);
final w = await realRsyncTree(rsync, src, want, opts);
final gone = [for (final k in before.keys) if (!w.containsKey(k)) k];
final delete = optionsDelete(opts);
final filter = RsyncFilter.parse(opts);
final why = <String>[];
// ① 미리 보기: 받는 쪽 트리 + 미리 보기 줄 → 예측한 트리
final pv = p.join(tmp.path, 'pv');
writeScene(src, pv, srcFiles: _src, dstFiles: _dst);
final items = await previewSync(src, pv, toRight: true, delete: delete, filter: filter);
final guess = Map.of(before);
for (final x in items) {
if (x.action == PreviewAction.delete) {
guess.removeWhere((k, _) => k == x.rel || k.startsWith('${x.rel}/'));
}
}
for (final x in items) {
if (x.action == PreviewAction.add || x.action == PreviewAction.update) {
guess[x.rel] = x.isDir ? '/' : File(p.joinAll([src, ...x.rel.split('/')])).readAsStringSync();
}
}
for (final d in treeDiff(w, guess)) {
why.add('미리 보기: $d');
}
// ② 원격 맞추기 (WebDAV)
final got = p.join(remote.path, 'm');
writeScene(src, got, srcFiles: _src, dstFiles: _dst);
await FileOps().mirror(src, 'dav://$id/m', delete: delete, filter: filter);
for (final d in treeDiff(w, treeOf(got))) {
why.add('원격 맞추기: $d');
}
// ③ · ④ 실시간 동기화 (새 쌍 - 지울 것이 있으면 묻고, 확인하면 맞춤)
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
c.settings
..copy.rsyncSource = 'custom'
..copy.rsyncPath = rsync
..copy.rsyncOptions = opts
..remote.webdavServers = [dav()];
for (final (label, target, local) in [('실시간 (로컬)', p.join(tmp.path, 'live'), true), ('실시간 (원격)', 'dav://$id/l', false)]) {
final dir = local ? target : p.join(remote.path, 'l');
writeScene(src, dir, srcFiles: _src, dstFiles: _dst);
final pair = LiveSyncPair(src, target, method: 'rsync', delete: delete);
c.settings.copy.liveSyncPairs = [pair];
final live = LiveSync(c);
final k = LiveSync.keyOf(pair);
await live.syncNow(pair);
if (live.status[k]!.$2.contains('실패')) why.add('$label 상태: ${live.status[k]!.$2}');
if (live.noAutoDelete[k] != null) why.add('$label: 미리 볼 수 없다고 함 ${live.noAutoDelete[k]}');
final asked = live.toDelete[k] ?? const <String>[];
for (final g in gone) {
if (!asked.any((a) => g == a || g.startsWith('$a/'))) why.add('$label: rsync 는 지우는데 묻지 않음: $g');
}
for (final a in asked) {
if (!gone.contains(a)) why.add('$label: rsync 는 지우지 않는데 물음: $a');
}
if (asked.isNotEmpty) await live.decideDelete(c.settings.copy.liveSyncPairs.single, delete: true);
for (final d in treeDiff(w, treeOf(dir))) {
why.add('$label: $d');
}
if ((live.pending[k] ?? const []).isNotEmpty) why.add('$label: 맞춘 뒤에도 맞출 것 ${live.pending[k]}');
live.dispose();
}
expect(why, isEmpty, reason: '$opts\n ${why.join('\n ')}');
}, timeout: const Timeout(Duration(minutes: 2)));
}
}