JJ_MKVMaker/test/explorer_modules_e5c_contra...

122 lines
7.0 KiB
Dart

import 'dart:io';
import 'package:archive/archive.dart' show Archive, ArchiveFile, ZipEncoder;
import 'package:flutter_test/flutter_test.dart';
import 'package:jj_mkvmaker/app/app_controller.dart';
import 'package:jj_mkvmaker/app/archive_browse.dart';
import 'package:jj_mkvmaker/app/copy_center.dart';
import 'package:jj_mkvmaker/app/file_opener.dart';
import 'package:jj_mkvmaker/app/rsync_runner.dart';
import 'package:jj_mkvmaker/core/app_hidden.dart';
import 'package:jj_mkvmaker/core/file_ops.dart' show FileEntry;
import 'package:jj_mkvmaker/core/sync_preview.dart' show PreviewAction;
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;
/// E5c 약속 시험: lib/app/archive_browse.dart · file_opener.dart · rsync_runner.dart · lib/core/app_hidden.dart 맨 위 약속.
/// 시험 폴더의 실제 파일 · 실제 ZIP (7z 는 동봉 7-Zip 이 있을 때), 설정은 메모리에만.
void main() {
late Directory tmp;
setUp(() => tmp = Directory.systemTemp.createTempSync('jj_e5c_'));
tearDown(() => tmp.deleteSync(recursive: true));
String zipOf(Map<String, String> files) {
final z = p.join(tmp.path, 'pack.zip');
final a = Archive();
files.forEach((k, v) => a.addFile(ArchiveFile.string(k, v)));
File(z).writeAsBytesSync(ZipEncoder().encode(a));
return z;
}
test('압축: 읽기 · 안의 목록 (폴더를 경로에서 만듦 · 위험한 이름 뺌) · 바뀌면 다시 읽고 잊음 알림 · 없어짐 · 하나 꺼내기', () async {
final z = zipOf({'2026/beach.jpg': 'jpg', 'readme.txt': 'hello', '../evil.txt': 'x'});
final forgot = <String>[];
final b = ArchiveBrowser(() async => null, onForget: forgot.add);
expect(ArchiveBrowser.canBrowse(FileEntry(z, isDir: false, modified: DateTime(0))), isTrue);
expect(await b.read(z), ArchiveRead.ok);
expect([for (final e in b.list(z)) '${p.basename(e.path)}${e.isDir ? '/' : ''}']..sort(), ['2026/', 'readme.txt']);
expect(b.list(p.join(z, '2026')).single.path, p.join(z, '2026', 'beach.jpg'));
expect([b.inside(p.join(z, 'readme.txt')), b.inside(z)], [true, false]);
expect(b.innerName(z, p.join(z, '2026', 'beach.jpg')), '2026/beach.jpg');
final (out, why) = await b.extractOne(p.join(z, 'readme.txt'), tmp.path);
expect(why, ArchiveRead.ok);
expect(File(out!).readAsStringSync(), 'hello');
// 바뀌면 다시 읽음 (잊음 알림)
await Future<void>.delayed(const Duration(milliseconds: 1100));
zipOf({'new.txt': 'n'});
expect(await b.read(z), ArchiveRead.ok);
expect(forgot, [z]);
expect([for (final e in b.list(z)) p.basename(e.path)], ['new.txt']);
File(z).deleteSync();
expect(await b.read(z), ArchiveRead.gone);
expect(b.contains(z), isFalse);
});
test('압축: 7z · RAR 은 7-Zip 이 없으면 noTool', () async {
final s = p.join(tmp.path, 'a.7z');
File(s).writeAsStringSync('not really');
expect(await ArchiveBrowser(() async => null).read(s), ArchiveRead.noTool);
});
test('여는 길: 동영상 · 7z · 그림 · PDF · ZIP · HWP · 문서 · 바깥 앱 (켠 컴포넌트 · 기기에 따라)', () {
OpenRoute r(String f, {List<String> comps = const ['viewer', 'docs'], bool android = false, bool windows = true, bool hwp = true}) =>
openRouteFor(f, components: comps, imageExts: const ['jpg', 'png'], android: android, windows: windows, hwpEngine: hwp);
expect(r('a.mkv'), OpenRoute.player);
expect(r('a.7z'), OpenRoute.extractArchive);
expect(r('a.jpg'), OpenRoute.images);
expect(r('a.jpg', comps: const []), OpenRoute.external, reason: '보기 컴포넌트가 꺼져 있음');
expect(r('a.pdf'), OpenRoute.pdf);
expect(r('a.cbz'), OpenRoute.zip);
expect(r('a.hwp', android: true, windows: false), OpenRoute.hwp);
expect(r('a.hwp', android: true, windows: false, hwp: false), OpenRoute.external);
expect(r('a.docx'), OpenRoute.docPreview);
expect(r('a.docx', windows: false), OpenRoute.external);
expect(r('a.txt'), OpenRoute.external);
});
test('목록에서 숨기기: 같은 경로 (대소문자) · 옮기면 새 경로 · 실제로 없어진 것만 뺌 · 바뀐 것 없으면 null', () async {
final d = p.join(tmp.path, 'Show');
Directory(p.join(d, 'sub')).createSync(recursive: true);
final keep = p.join(tmp.path, 'keep.txt');
File(keep).writeAsStringSync('k');
final list = [p.join(d, 'sub'), keep, p.join(tmp.path, 'gone.txt')];
final h = HiddenList();
expect(h.isHidden(list, Platform.isWindows ? keep.toUpperCase() : keep), isTrue);
expect(h.isHidden(list, p.join(tmp.path, 'x')), isFalse);
final moved = await HiddenList.follow(list, moved: [(d, p.join(tmp.path, 'Season'))], gone: [tmp.path]);
expect(moved, [p.join(tmp.path, 'Season', 'sub'), keep], reason: 'gone.txt 은 없음 → 뺌, keep.txt 은 있음 → 남김');
expect(await HiddenList.follow(list, moved: [(p.join(tmp.path, 'zzz'), p.join(tmp.path, 'yyy'))]), isNull);
});
test('Rsync 실행: 막는 이유 · 쌍 · ⇄ 의 옵션 (← 는 지우지 않음 · -u) · 원본 지우기 · 미리 보기 (실제 폴더)', () async {
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
c.settings.copy.rsyncOptions = '-a --delete';
final center = CopyCenter.of(c);
final l = p.join(tmp.path, 'L'), r = p.join(tmp.path, 'R');
Directory(l).createSync();
Directory(r).createSync();
File(p.join(l, 'a.txt')).writeAsStringSync('a');
File(p.join(r, 'old.txt')).writeAsStringSync('o');
({RsyncRun? run, String? problem, bool missing}) prep(String? a, String? b, {bool right = true, bool left = false, bool inner = true}) =>
RsyncRun.prepare(center, l: a, r: b, right: right, left: left, allowInnerToOuter: inner, jobs: const [], viaDav: false, copySkipNewer: false);
expect(prep(null, r).problem, isNotNull);
expect(prep(l, p.join(l, 'in')).problem, isNotNull, reason: '대상이 원본 안');
Directory(p.join(r, 'sub')).createSync();
expect(prep(p.join(r, 'sub'), r, inner: false).problem, isNotNull, reason: '원본이 대상 안 (설정이 막음)');
expect(prep(p.join(r, 'sub'), r).problem, RsyncRun.nestedDeleteMessage, reason: '원본이 대상 안 + --delete');
final gone = prep(p.join(tmp.path, 'none'), r);
expect([gone.problem != null, gone.missing], [true, true]);
final one = prep(l, r).run!;
expect([one.runs.length, one.both, one.deletes()], [1, false, true]);
final items = await one.preview();
expect(items.any((x) => x.action == PreviewAction.delete), isTrue, reason: '실제 폴더 비교: 받는 쪽에만 있는 것은 지움');
final two = prep(l, r, left: true).run!;
expect(two.runOpts(0), contains('-u'));
expect(two.runOpts(1), isNot(contains('--delete')), reason: '← 에서는 지우지 않음');
one.setRemoveSource(true);
expect([one.removeSource, one.tasks.single.move], [true, true]);
});
}