194 lines
10 KiB
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
|
|
..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)));
|
|
}
|
|
}
|