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 --delete --exclude=*.log', '-a --delete --exclude=*.log'), ('화면 글의 U+2060', '-a --delete --exclude=⁠*.log', '-a --delete --exclude=*.log'), ('U+200B', '-a --delete --excl​ude=*.log', '-a --delete --exclude=*.log'), ('줄바꿈 · 탭', '-a\t--delete\n--exclude=*.log\r\n', '-a --delete --exclude=*.log'), ('전각 빈칸 (U+3000)', '-a --delete --exclude=*.log', '-a --delete --exclude=*.log'), ('en dash (–delete)', '-a –delete --exclude=*.log', '-a --delete --exclude=*.log'), ('em dash (—delete)', '-a —delete --exclude=*.log', '-a --delete --exclude=*.log'), ('굽은 큰따옴표', '-a --delete --filter=“- *.log”', '-a --delete --filter="- *.log"'), ('굽은 작은따옴표', '-a --delete --exclude=‘*.log’', "-a --delete --exclude='*.log'"), ('소프트 하이픈 (U+00AD)', '-a --del­ete --exclude=*.log', '-a --delete --exclude=*.log'), ('줄바꿈 없는 하이픈 (U+2011)', '-a ‑‑delete --exclude=*.log', '-a --delete --exclude=*.log'), ('전각 하이픈 (U+FF0D)', '-a --delete --exclude=*.log', '-a --delete --exclude=*.log'), ('BOM 앞', '-a --delete --exclude=*.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’s.txt'), ['--exclude=it’s.txt']); expect(splitOptions('--exclude=a–b.txt'), ['--exclude=a–b.txt']); // 굽은 따옴표 안의 굽은 따옴표 (뒤가 빈칸 · 끝이 아니면 닫지 않음) expect(splitOptions('--exclude=‘it’s a.txt’ -a'), ['--exclude=it’s a.txt', '-a']); // 짧은 옵션 뒤 (-f“- *.log”) expect(splitOptions('-f“- *.log”'), ['-f- *.log']); // 대시 하나 + 짧은 글자는 - (–av → -av), 소문자 낱말이면 -- (–delete → --delete, –bwlimit=100 → --bwlimit=100) expect(splitOptions('–av –delete –bwlimit=100'), ['-av', '--delete', '--bwlimit=100']); }); test('고쳐 읽어도 알아볼 수 없는 옵션 (ASCII 밖 글자로 시작) 은 알린다 - 옵션의 값 자리는 세지 않음', () { expect(unreadableOptions('-a «delete»'), ['«delete»']); expect(unreadableOptions('-a …delete'), ['…delete']); 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); }); testWidgets('알아볼 수 없는 옵션 알림 (빨간 글, 옵션 글은 줄바꿈 없이)', (tester) async { await tester.pumpWidget(const MaterialApp(home: Scaffold(body: OptionProblemNote(['«delete»'])))); expect(find.textContaining('옵션을 알아볼 수 없습니다'), findsOneWidget); }); // 실제 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> 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); }); } }); }