178 lines
12 KiB
Dart
178 lines
12 KiB
Dart
import 'dart:io';
|
||
|
||
import 'package:flutter/material.dart';
|
||
import 'package:flutter_test/flutter_test.dart';
|
||
import 'package:jj_mkvmaker/core/rsync_filter.dart';
|
||
import 'package:jj_mkvmaker/core/sync_preview.dart' show optionsDelete;
|
||
import 'package:jj_mkvmaker/core/sync_tools.dart';
|
||
import 'package:jj_mkvmaker/ui/sync_preview_view.dart';
|
||
import 'package:path/path.dart' as p;
|
||
|
||
import 'support/rsync_scenes.dart';
|
||
|
||
/// 관리자 지시 (10/20, 28 확인): 웹 · 문서에서 옵션 칸에 붙여 넣은 글 (굽은 따옴표 · 대시 · 보이지 않는 글자) 을
|
||
/// 곧은 글자로 쓴 것과 똑같이 읽는다. 예전에는 --exclude=‘*.log’ 가 아무것도 거르지 못해 받는 쪽 keep.log 가 지워졌고,
|
||
/// –delete 는 원본 경로로 넘어가 --delete 가 몰래 빠졌다. 보이지 않는 글자는 \u 표기로 쓴다
|
||
void main() {
|
||
// 28 의 13가지: (이름, 붙여 넣은 글, 곧은 글자로 쓴 것)
|
||
const cases = <(String, String, String)>[
|
||
('줄바꿈 없는 빈칸 (U+00A0)', '-a\u00A0--delete\u00A0--exclude=*.log', '-a --delete --exclude=*.log'),
|
||
('화면 글의 U+2060', '-a --delete --exclude=\u2060*.log', '-a --delete --exclude=*.log'),
|
||
('U+200B', '-a --delete --excl\u200Bude=*.log', '-a --delete --exclude=*.log'),
|
||
('줄바꿈 · 탭', '-a\t--delete\n--exclude=*.log\r\n', '-a --delete --exclude=*.log'),
|
||
('전각 빈칸 (U+3000)', '-a\u3000--delete\u3000--exclude=*.log', '-a --delete --exclude=*.log'),
|
||
('en dash (\u2013delete)', '-a \u2013delete --exclude=*.log', '-a --delete --exclude=*.log'),
|
||
('em dash (\u2014delete)', '-a \u2014delete --exclude=*.log', '-a --delete --exclude=*.log'),
|
||
('굽은 큰따옴표', '-a --delete --filter=\u201C- *.log\u201D', '-a --delete --filter="- *.log"'),
|
||
('굽은 작은따옴표', '-a --delete --exclude=\u2018*.log\u2019', "-a --delete --exclude='*.log'"),
|
||
('소프트 하이픈 (U+00AD)', '-a --del\u00ADete --exclude=*.log', '-a --delete --exclude=*.log'),
|
||
('줄바꿈 없는 하이픈 (U+2011)', '-a \u2011\u2011delete --exclude=*.log', '-a --delete --exclude=*.log'),
|
||
('전각 하이픈 (U+FF0D)', '-a \uFF0D\uFF0Ddelete --exclude=*.log', '-a --delete --exclude=*.log'),
|
||
('BOM 앞', '\uFEFF-a --delete --exclude=*.log', '-a --delete --exclude=*.log'),
|
||
// 15 확인 (10/20): F2 짧은 옵션 묶음 앞의 대시 하나 · F3 거르기 규칙 머리의 대시 · F5 닫는 따옴표 뒤에 글
|
||
('F2 \u2013avz', '\u2013avz --delete --exclude=*.log', '-avz --delete --exclude=*.log'),
|
||
('F2 \u2013rlpt', '\u2013rlpt --delete --exclude=*.log', '-rlpt --delete --exclude=*.log'),
|
||
('F3 --filter=\u201C\u2013 *.log\u201D', '-a --delete --filter=\u201C\u2013 *.log\u201D', '-a --delete --filter="- *.log"'),
|
||
('F3 -f\u201C\u2013 *.log\u201D', '-a --delete -f\u201C\u2013 *.log\u201D', '-a --delete -f"- *.log"'),
|
||
('F3 --filter \u201C\u2013 *.log\u201D (값 따로)', '-a --delete --filter \u201C\u2013 *.log\u201D', '-a --delete --filter "- *.log"'),
|
||
('F5 \u201C*\u201D.log', '-a --delete --exclude=\u201C*\u201D.log', '-a --delete --exclude="*".log'),
|
||
];
|
||
|
||
group('붙여 넣은 옵션 = 곧은 글자로 쓴 것', () {
|
||
for (final (name, pasted, straight) in cases) {
|
||
test(name, () {
|
||
// 옵션 칸 (OptionTextFormatter) 을 거친 글도, 거치지 않은 글 (예전에 저장된 값) 도
|
||
final typed = const OptionTextFormatter()
|
||
.formatEditUpdate(TextEditingValue.empty, TextEditingValue(text: pasted, selection: TextSelection.collapsed(offset: pasted.length)))
|
||
.text;
|
||
for (final s in [pasted, typed]) {
|
||
expect(splitOptions(s), splitOptions(straight));
|
||
expect(unreadableOptions(s), isEmpty);
|
||
expect(optionsDelete(s), optionsDelete(straight));
|
||
final f = RsyncFilter.parse(s), g = RsyncFilter.parse(straight);
|
||
expect(f.excluded('keep.log', isDir: false), g.excluded('keep.log', isDir: false));
|
||
expect(f.excluded('a.txt', isDir: false), isFalse);
|
||
}
|
||
});
|
||
}
|
||
});
|
||
|
||
test('낱말 가운데 · 이름 안은 그대로 (파일 이름일 수 있음)', () {
|
||
// 이름 안의 굽은 작은따옴표 (it’s) · 대시 (a–b) 는 따옴표 · 옵션으로 읽지 않는다
|
||
expect(splitOptions('--exclude=it\u2019s.txt'), ['--exclude=it\u2019s.txt']);
|
||
expect(splitOptions('--exclude=a\u2013b.txt'), ['--exclude=a\u2013b.txt']);
|
||
// 굽은 따옴표 안의 굽은 따옴표 (뒤가 빈칸 · 끝이 아니면 닫지 않음)
|
||
expect(splitOptions('--exclude=\u2018it\u2019s a.txt\u2019 -a'), ['--exclude=it\u2019s a.txt', '-a']);
|
||
// 짧은 옵션 뒤 (-f“- *.log”)
|
||
expect(splitOptions('-f\u201C- *.log\u201D'), ['-f- *.log']);
|
||
// 대시 하나 + 짧은 글자는 - (–av → -av), 소문자 낱말이면 -- (–delete → --delete, –bwlimit=100 → --bwlimit=100)
|
||
expect(splitOptions('\u2013av \u2013delete \u2013bwlimit=100'), ['-av', '--delete', '--bwlimit=100']);
|
||
});
|
||
|
||
test('15 확인 F1 (데이터): 닫히지 않은 따옴표는 뒤의 옵션을 삼키지 않고, 알려서 실행을 막는다 (굽은 · 곧은 모두)', () {
|
||
for (final q in ['\u2018', '\u201C', '"', "'"]) {
|
||
final o = '-a --delete --exclude=${q}a.tmp --exclude=keep.txt';
|
||
expect(splitOptions(o), ['-a', '--delete', '--exclude=${q}a.tmp', '--exclude=keep.txt'], reason: q);
|
||
expect(unreadableOptions(o).single, contains('따옴표가 닫히지 않았습니다'), reason: q);
|
||
expect(RsyncFilter.parse(o).excluded('keep.txt', isDir: false), isTrue, reason: '뒤의 규칙이 살아 있음');
|
||
}
|
||
// 닫힌 것은 문제 없음
|
||
expect(unreadableOptions('-a --exclude="a b.txt" --exclude=\u2018c d\u2019'), isEmpty);
|
||
});
|
||
|
||
test('15 확인 F4 · F5: 값 자리의 대시는 그대로, 닫는 굽은 따옴표 뒤에 글이 이어져도 닫는다', () {
|
||
expect(splitOptions('--exclude \u2013x.txt -a'), ['--exclude', '\u2013x.txt', '-a']);
|
||
expect(splitOptions('-e \u2013x'), ['-e', '\u2013x']);
|
||
expect(splitOptions('--exclude=\u201Ca b\u201D.txt'), ['--exclude=a b.txt']);
|
||
expect(splitOptions('--exclude=\u2018a b\u2019.txt'), ['--exclude=a b.txt']);
|
||
// 긴 옵션 이름 (–no-perms · –dry-run) 은 --, 목록에 없는 것은 짧은 옵션 묶음
|
||
expect(splitOptions('\u2013no-perms \u2013dry-run \u2013avh'), ['--no-perms', '--dry-run', '-avh']);
|
||
});
|
||
|
||
test('고쳐 읽어도 알아볼 수 없는 옵션 (ASCII 밖 글자로 시작) 은 알린다 - 옵션의 값 자리는 세지 않음', () {
|
||
expect(unreadableOptions('-a \u00ABdelete\u00BB'), ['경로로 읽힙니다: \u00ABdelete\u00BB']);
|
||
expect(unreadableOptions('-a \u2026delete'), ['경로로 읽힙니다: \u2026delete']);
|
||
expect(unreadableOptions('--exclude 한글.txt -a'), isEmpty, reason: '--exclude 의 값');
|
||
expect(unreadableOptions('-f 한글.txt'), isEmpty, reason: '-f 의 값');
|
||
expect(unreadableOptions('-a 한글'), ['경로로 읽힙니다: 한글'], reason: '옵션 자리 - rsync 는 원본 경로로 읽는다');
|
||
expect(unreadableOptions('-avPog --delete'), isEmpty);
|
||
// 관리자 판단 (10/20, 28 확인): - 로 시작하지 않는 낱말 · 하이픈 셋 이상 · 끝에 값 없는 옵션
|
||
expect(unreadableOptions('-a delete'), ['경로로 읽힙니다: delete']);
|
||
expect(unreadableOptions('-a - --delete'), ['경로로 읽힙니다: -']);
|
||
expect(unreadableOptions('-a ---delete'), ['---delete']);
|
||
expect(unreadableOptions('-a --exclude'), ['--exclude 의 값이 없습니다']);
|
||
expect(unreadableOptions('-a -f'), ['-f 의 값이 없습니다']);
|
||
expect(unreadableOptions('-a --exclude *.log --delete'), isEmpty, reason: '값이 있음');
|
||
expect(unreadableOptions('-a --exclude=*.log'), isEmpty);
|
||
expect(unreadableOptions(''), isEmpty);
|
||
// 28 확인 (10/20): rsync 의 짧은 별칭 (--cc · --zc · --zl) 도 값을 받는다 (오탐이던 것)
|
||
expect(unreadableOptions('-a --cc md5 --zc zlib --zl 3 --zt 2'), isEmpty);
|
||
// 곧은 하이픈과 대시가 섞인 것 (-\u2013delete)
|
||
expect(splitOptions('-a -\u2013delete \u2013-del'), ['-a', '--delete', '--del']);
|
||
expect(unreadableOptions('-a -\u2013delete'), isEmpty);
|
||
});
|
||
|
||
test('28 지적 (10/20): 안내 끝 문구는 문제 종류에 맞게', () {
|
||
String hint(String o) => optionProblemHint(optionProblems(o));
|
||
expect(hint('-a --exclude="a.tmp'), '따옴표를 닫아 주세요');
|
||
expect(hint('-a delete'), '옵션은 - 나 -- 로 시작합니다');
|
||
expect(hint('-a --exclude'), '값을 적어 주세요 (예: --exclude=*.tmp)');
|
||
expect(hint('-a ---delete'), '곧은 따옴표와 하이픈 (-) 으로 다시 적어 주세요');
|
||
expect(hint('-a \u00ABx\u00BB'), '곧은 따옴표와 하이픈 (-) 으로 다시 적어 주세요');
|
||
expect(hint('-a delete --exclude'), '옵션은 - 나 -- 로 시작합니다 · 값을 적어 주세요 (예: --exclude=*.tmp)');
|
||
expect(optionProblemMessage(optionProblems('-a delete')), '옵션을 알아볼 수 없습니다: 경로로 읽힙니다: delete - 옵션은 - 나 -- 로 시작합니다');
|
||
});
|
||
|
||
testWidgets('알아볼 수 없는 옵션 알림 (빨간 글, 옵션 글은 줄바꿈 없이)', (tester) async {
|
||
await tester.pumpWidget(MaterialApp(home: Scaffold(body: OptionProblemNote(optionProblems('-a \u00ABdelete\u00BB')))));
|
||
expect(find.textContaining('옵션을 알아볼 수 없습니다'), findsOneWidget);
|
||
expect(find.textContaining('곧은 따옴표와 하이픈'), findsOneWidget);
|
||
});
|
||
|
||
// 28 확인 (10/20): 막기 전 실제 rsync 는 이런 낱말을 원본 경로로 먹는다 - 그래서 [unreadableOptions] 로 막는다
|
||
test('실제 rsync: -a delete 의 delete · 끝의 --exclude 는 원본 경로로 먹혀 의도와 다르게 돈다', () async {
|
||
final rsync = sceneRsync;
|
||
if (rsync == null) return markTestSkipped('앱에 든 rsync 없음 (Windows · third_party)');
|
||
final tmp = Directory.systemTemp.createTempSync('jj_optpath_');
|
||
addTearDown(() => tmp.deleteSync(recursive: true));
|
||
final s = p.join(tmp.path, 's'), d = p.join(tmp.path, 'd');
|
||
Future<ProcessResult> run(String opts) {
|
||
writeScene(s, d, srcFiles: const ['a.txt'], dstFiles: const ['old.txt']);
|
||
return Process.run(rsync, [...splitOptions(opts), '${toCygwinPath(s)}/', '${toCygwinPath(d)}/'], workingDirectory: tmp.path);
|
||
}
|
||
|
||
// "delete" 를 원본으로 찾다 실패 (exit 23) - --delete 를 뜻했지만 old.txt 는 그대로
|
||
final r1 = await run('-r delete');
|
||
expect(r1.exitCode, isNot(0));
|
||
expect(File(p.join(d, 'old.txt')).existsSync(), isTrue);
|
||
// --exclude 가 원본 폴더를 값으로 먹고 받는 쪽 하나만 남아 목록만 보이고 끝 (아무것도 보내지 않음)
|
||
final r2 = await run('-r --exclude');
|
||
expect(r2.exitCode, 0);
|
||
expect(File(p.join(d, 'a.txt')).existsSync(), isFalse, reason: '보내지 않음');
|
||
expect(unreadableOptions('-r delete'), isNotEmpty);
|
||
expect(unreadableOptions('-r --exclude'), isNotEmpty);
|
||
});
|
||
|
||
// 실제 rsync (Windows, 앱에 든 것): 붙여 넣은 글로 맞춘 결과 = 곧은 글자로 맞춘 결과 (28 의 장면: 받는 쪽 keep.log · old.txt)
|
||
group('실제 rsync 결과가 같다', () {
|
||
for (final (name, pasted, straight) in cases) {
|
||
test(name, () async {
|
||
final rsync = sceneRsync;
|
||
if (rsync == null) return markTestSkipped('앱에 든 rsync 없음 (Windows · third_party)');
|
||
final tmp = Directory.systemTemp.createTempSync('jj_optpaste_');
|
||
addTearDown(() => tmp.deleteSync(recursive: true));
|
||
Future<Map<String, String>> run(String opts, String tag) async {
|
||
final s = p.join(tmp.path, '${tag}s'), d = p.join(tmp.path, '${tag}d');
|
||
writeScene(s, d, srcFiles: const ['a.txt', 'b.log'], dstFiles: const ['old.txt', 'keep.log']);
|
||
return realRsyncTree(rsync, s, d, opts);
|
||
}
|
||
|
||
final want = await run(straight, 'w');
|
||
expect(want.containsKey('keep.log'), isTrue, reason: '곧은 글자: *.log 는 거르므로 받는 쪽 keep.log 를 지키고');
|
||
expect(want.containsKey('old.txt'), isFalse, reason: '--delete 로 old.txt 는 지움');
|
||
expect(await run(pasted, 'g'), want);
|
||
});
|
||
}
|
||
});
|
||
}
|