JJ_MKVMaker/test/option_paste_test.dart

178 lines
12 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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);
});
}
});
}