From 5366bb3a14c1ed0f6e8594bf2a582a47cca17f01 Mon Sep 17 00:00:00 2001 From: RyuJongJin <266630912+RyuJongJin@users.noreply.github.com> Date: Tue, 20 Oct 2026 23:21:07 +0900 Subject: [PATCH] =?UTF-8?q?=EC=9B=90=EA=B2=A9=20=EC=8B=9C=ED=97=98=20?= =?UTF-8?q?=ED=9D=94=EB=93=A4=EB=A6=BC:=20"=EC=A3=BC=EC=86=8C=EC=97=90=20?= =?UTF-8?q?=EB=B9=84=EB=B0=80=EB=B2=88=ED=98=B8=20=EC=97=86=EC=9D=8C"=20?= =?UTF-8?q?=EC=9D=B4=20=EB=AC=B4=EC=9E=91=EC=9C=84=20=ED=91=9C=EC=97=90=20?= =?UTF-8?q?=EA=B1=B8=EB=A6=AC=EB=8D=98=20=EA=B2=83=20=C2=B7=20FTP=20=3D=20?= =?UTF-8?q?rsync=20=EB=B9=84=EA=B5=90=20=EC=8B=A4=ED=8C=A8=20=EB=95=8C=20?= =?UTF-8?q?=EC=9E=90=EC=84=B8=ED=9E=88=20(=EA=B4=80=EB=A6=AC=EC=9E=90=20?= =?UTF-8?q?=EC=A7=80=EC=8B=9C=2010/20,=2028=20=EC=9D=B4=20=EC=B0=BE?= =?UTF-8?q?=EC=9D=8C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 원인 (28 의 hwp2 전체 시험 원문): 실패한 것은 "FTP 맞추기 결과 = 실제 rsync 결과" 가 아니라 다음 시험 "FTP 동영상 … 다리로 재생". 시험 비밀번호가 두 글자 "pw" 인데 다리 주소의 무작위 표 (…-4pwvUR-…) 에 우연히 들어가 isNot(contains('pw')) 가 실패 (대략 1% 안팎). 비밀번호가 샌 것이 아님 - 앱은 고치지 않음 - ftp · sftp_stream · smb 시험: 길고 드문 비밀번호로 보고, 주소의 사용자 정보 (userinfo) 가 비었는지도 따로 - ftp_test "FTP 맞추기 결과 = 실제 rsync 결과": 실패하면 옵션 · 걸린 시간 · 포트 · 예외 · 다른 점과 보내는 쪽 · 맞추기 전 앱 쪽 · rsync 쪽 · 앱 쪽 목록 (크기 · 바뀐 시각 ms) 을 한 번에. 맞추기가 예외를 던져도 어느 옵션인지 남김 - 확인: 혼자 12번 · 전체 단위 시험 (1143 통과) · 원격 5개 파일 -j 8 로 4번 모두 통과 Co-Authored-By: Claude Opus 5.5 --- test/ftp_test.dart | 59 ++++++++++++++++++++++++++++++++++---- test/sftp_stream_test.dart | 12 ++++++-- test/smb_test.dart | 14 +++++---- 3 files changed, 72 insertions(+), 13 deletions(-) diff --git a/test/ftp_test.dart b/test/ftp_test.dart index 8f2d24c..9cf6ac2 100644 --- a/test/ftp_test.dart +++ b/test/ftp_test.dart @@ -22,6 +22,9 @@ import 'package:jj_mkvmaker/services/platform_services.dart'; /// l (관리자 지시 10/17): FTP · FTPS (dart:io 로 직접). 실제 서버 시험은 tool/ftp_test_server.py (pyftpdlib) - /// 시험용 파이썬 (sftp 시험과 같은 venv) 에 pyftpdlib 가 없으면 건너뛴다 +/// 시험 서버 비밀번호 - 무작위 주소에 우연히 들어가지 않게 길고 드물게 (관리자 지시 10/20, 28 이 찾음) +const ftpPw = 'ftp-Pw-7Rk2xQ9t'; + void main() { final hasPy = File(sftpTestPython).existsSync(); @@ -76,7 +79,8 @@ void main() { late String caFp; Future start(String mode, {bool chain = false}) async { - final pr = await Process.start(sftpTestPython, ['tool/ftp_test_server.py', '--root', root.path, '--mode', mode, if (chain) '--chain']); + final pr = await Process.start( + sftpTestPython, ['tool/ftp_test_server.py', '--root', root.path, '--mode', mode, '--password', ftpPw, if (chain) '--chain']); proc = pr; final lines = pr.stdout.transform(utf8.decoder).transform(const LineSplitter()).asBroadcastStream(); pr.stderr.drain(); @@ -111,7 +115,7 @@ void main() { } }); - FtpServer srv({String security = 'explicit', String cert = '', String password = 'pw'}) => + FtpServer srv({String security = 'explicit', String cert = '', String password = ftpPw}) => FtpServer(id: 'f1', name: 'test', host: '127.0.0.1', port: port, user: 'jj', password: password, security: security, certFingerprint: cert); Future err(Future Function() f) async { @@ -247,13 +251,55 @@ void main() { FtpRegistry.configure([srv(security: 'none')]); final src = p.join(tmp.path, 'local', 'scene_src'), want = p.join(tmp.path, 'local', 'scene_rsync'), got = p.join(root.path, 'scene'); final bad = []; + // 관리자 지시 (10/20): 가끔 실패해 원인을 보려고, 실패하면 옵션 · 걸린 시간 · 포트 · 예외 · 다른 점과 + // 보내는 쪽 · 맞추기 전 앱 쪽 · rsync 쪽 · 앱 쪽 목록 (파일은 크기 · 바뀐 시각 ms) 을 한 번에 보인다 + String listing(String dir) { + final root = Directory(dir); + if (!root.existsSync()) return ' (없음)'; + final rows = []; + for (final e in root.listSync(recursive: true)) { + final rel = p.relative(e.path, from: dir).replaceAll(r'\', '/'); + if (e is Directory) { + rows.add('$rel/'); + } else { + final s = e.statSync(); + rows.add('$rel ${s.size}B ${s.modified.toIso8601String()}'); + } + } + return (rows..sort()).map((r) => ' $r').join('\n'); + } + + final sw = Stopwatch()..start(); for (final opts in sceneOptions) { + final t0 = sw.elapsedMilliseconds; writeScene(src, want); final w = await realRsyncTree(rsync, src, want, opts); + final wantList = listing(want); writeScene(src, got); - await FileOps().mirror(src, 'ftp://f1/scene', delete: optionsDelete(opts), filter: RsyncFilter.parse(opts)); + final beforeList = listing(got); + Object? error; + try { + await FileOps().mirror(src, 'ftp://f1/scene', delete: optionsDelete(opts), filter: RsyncFilter.parse(opts)); + } catch (e, st) { + error = '$e\n$st'; + } final d = treeDiff(w, treeOf(got)); - if (d.isNotEmpty) bad.add('$opts\n ${d.join('\n ')}'); + if (d.isNotEmpty || error != null) { + bad.add([ + '옵션: $opts (${sw.elapsedMilliseconds - t0}ms, 시험 시작부터 ${t0}ms, 포트 $port, ${DateTime.now().toIso8601String()})', + if (error != null) ' 예외: $error', + ' 다른 점:', + ...d.map((x) => ' $x'), + ' 보내는 쪽:', + listing(src), + ' 맞추기 전 앱 쪽:', + beforeList, + ' rsync 쪽:', + wantList, + ' 앱 쪽:', + listing(got), + ].join('\n')); + } } expect(bad, isEmpty, reason: bad.join('\n')); }, skip: hasPy ? false : '시험용 파이썬 없음', timeout: const Timeout(Duration(minutes: 5))); @@ -285,7 +331,10 @@ void main() { final bridge = await RemoteStreamBridge.get(); final u1 = bridge.url('ftp://f1/영상/1화.mkv'), u2 = bridge.url('ftp://f1/영상/2화.mkv'); expect(u1, startsWith('http://127.0.0.1:')); - expect(u1, isNot(contains('pw')), reason: '주소에 비밀번호 없음'); + // 관리자 지시 (10/20, 28 이 찾음): 시험 비밀번호가 두 글자 "pw" 라 다리 주소의 무작위 표 (예 …-4pwvUR-…) 에 + // 우연히 들어가 "주소에 비밀번호 없음" 이 가끔 실패했다 (비밀번호가 샌 것이 아님) - 길고 드문 비밀번호로 보고, 주소의 사용자 정보도 따로 + expect(u1, isNot(contains(ftpPw)), reason: '주소에 비밀번호 없음'); + expect(Uri.parse(u1).userInfo, isEmpty, reason: '주소에 사용자 정보 없음'); Future<(int, String)> get(String url, [String? range]) async { final http = HttpClient(); try { diff --git a/test/sftp_stream_test.dart b/test/sftp_stream_test.dart index add6899..b1b29fa 100644 --- a/test/sftp_stream_test.dart +++ b/test/sftp_stream_test.dart @@ -16,6 +16,9 @@ import 'support/sftp_server.dart'; /// sftp_next: SFTP 동영상을 받지 않고 이 기기 안의 다리 (127.0.0.1) 로 스트리밍 - 같은 폴더의 다음 동영상 · 그 자막. /// 실제 서버 시험은 tool/sftp_test_server.py (paramiko) - 파이썬이 없으면 건너뛴다 (sftp_test 와 같음) +/// 시험 서버 비밀번호 - 무작위 주소에 우연히 들어가지 않게 길고 드물게 (관리자 지시 10/20, 28 이 찾음) +const sftpPw = 'sftp-Pw-3Lm8vZ2w'; + void main() { final hasServer = sftpTestServerAvailable; late Directory tmp; @@ -26,10 +29,10 @@ void main() { if (!hasServer) return; tmp = Directory.systemTemp.createTempSync('jj_sftpnext_'); root = Directory(p.join(tmp.path, 'root'))..createSync(); - final s = await SftpTestServer.start(root: root.path, hostKeyFile: p.join(tmp.path, 'hostkey')); + final s = await SftpTestServer.start(root: root.path, hostKeyFile: p.join(tmp.path, 'hostkey'), password: sftpPw); server = s; SftpRegistry.configure([ - SftpServer(id: 's1', name: 'test', host: '127.0.0.1', port: s.port, user: 'jj', password: 'pw', hostKeyFingerprint: s.fingerprint), + SftpServer(id: 's1', name: 'test', host: '127.0.0.1', port: s.port, user: 'jj', password: sftpPw, hostKeyFingerprint: s.fingerprint), ]); Directory(p.join(root.path, '영상')).createSync(); File(p.join(root.path, '영상', '1화.mkv')).writeAsStringSync('0123456789'); @@ -93,7 +96,10 @@ void main() { expect(u1, endsWith('/${Uri.encodeComponent('1화.mkv')}'), reason: 'mpv 가 확장자로 형식을 알게'); expect(u1, isNot(equals(u2))); expect(bridge.url('sftp://s1/영상/1화.mkv'), u1, reason: '같은 경로는 같은 주소'); - expect(u1, isNot(contains('pw')), reason: '주소에 비밀번호 없음'); + // 관리자 지시 (10/20, 28 이 찾음): 시험 비밀번호가 두 글자 "pw" 라 다리 주소의 무작위 표 (예 …-4pwvUR-…) 에 + // 우연히 들어가 "주소에 비밀번호 없음" 이 가끔 실패했다 (비밀번호가 샌 것이 아님) - 길고 드문 비밀번호로 보고, 주소의 사용자 정보도 따로 + expect(u1, isNot(contains(sftpPw)), reason: '주소에 비밀번호 없음'); + expect(Uri.parse(u1).userInfo, isEmpty, reason: '주소에 사용자 정보 없음'); var (code, h, body) = await get(u1); expect((code, body), (200, '0123456789')); diff --git a/test/smb_test.dart b/test/smb_test.dart index 1ce69d4..4361849 100644 --- a/test/smb_test.dart +++ b/test/smb_test.dart @@ -124,7 +124,8 @@ void main() { SmbRegistry.configure(const []); }); - const server = SmbServer(id: 'm1', name: 'NAS', host: 'nas', share: 'video', user: 'jj', password: 'pw'); + // 길고 드문 비밀번호 - 다리 주소의 무작위 표에 우연히 들어가지 않게 (관리자 지시 10/20, 28 이 찾음) + const server = SmbServer(id: 'm1', name: 'NAS', host: 'nas', share: 'video', user: 'jj', password: 'smb-Pw-5Qh9kT4r'); test('smb:// 로 폴더 · 쓰기 (1MB 조각) · 읽기 · 목록 · 이름 바꾸기 (덮어쓰기) · 지우기 · 화면 이름', () async { SmbRegistry.configure([server]); @@ -196,10 +197,10 @@ void main() { final secrets = MemorySecretStore(); final store = SettingsStore(p.join(tmp.path, 'settings.json'), secrets); await store.save(AppSettings()..smbServers = [server]); - expect(File(p.join(tmp.path, 'settings.json')).readAsStringSync(), isNot(contains('"pw"'))); - expect(secrets.values['smb.pw.m1'], 'pw'); + expect(File(p.join(tmp.path, 'settings.json')).readAsStringSync(), isNot(contains('smb-Pw-5Qh9kT4r'))); + expect(secrets.values['smb.pw.m1'], 'smb-Pw-5Qh9kT4r'); final back = (await SettingsStore(p.join(tmp.path, 'settings.json'), secrets).load()).smbServers.single; - expect((back.host, back.share, back.user, back.password), ('nas', 'video', 'jj', 'pw')); + expect((back.host, back.share, back.user, back.password), ('nas', 'video', 'jj', 'smb-Pw-5Qh9kT4r')); }); test('경로 입력의 smb:// · \\\\서버\\공유 → 서버 추가 창에 채울 (서버, 공유, 폴더)', () { @@ -329,7 +330,10 @@ void main() { final bridge = await RemoteStreamBridge.get(); final u1 = bridge.url('smb://m1/영상/1화.mkv'), u2 = bridge.url('smb://m1/영상/2화.mkv'); expect(u1, startsWith('http://127.0.0.1:')); - expect(u1, isNot(contains('pw')), reason: '주소에 비밀번호 없음'); + // 관리자 지시 (10/20, 28 이 찾음): 시험 비밀번호가 두 글자 "pw" 라 다리 주소의 무작위 표 (예 …-4pwvUR-…) 에 + // 우연히 들어가 "주소에 비밀번호 없음" 이 가끔 실패했다 (비밀번호가 샌 것이 아님) - 길고 드문 비밀번호로 보고, 주소의 사용자 정보도 따로 + expect(u1, isNot(contains(server.password)), reason: '주소에 비밀번호 없음'); + expect(Uri.parse(u1).userInfo, isEmpty, reason: '주소에 사용자 정보 없음'); Future<(int, String)> get(String url, [String? range]) async { final http = HttpClient(); try {