휴지통 시험: 기본은 가짜 휴지통, 실제 휴지통은 JJ_TEST_REAL_RECYCLE=1 일 때만
전체 시험 도중에 끊기면 사용자 휴지통에 시험 흔적이 남았다 (10/10 01:31). - 탐색기는 recycle() · restoreRecycled() 를 쓴다 (앱은 늘 실제 휴지통, 시험만 recycleBinForTest 로 바꿈). - test/support/fake_recycle_bin.dart: 실제와 같은 규칙 (긴 경로 거부 · 되돌릴 때 덮지 않음 · 원래 폴더 없으면 안 만듦), 다른 드라이브면 복사 후 지움. useTestRecycleBin() 은 JJ_TEST_REAL_RECYCLE=1 이 아니면 가짜로. - recycle_bin_test 의 실제 휴지통 시험 3개와 긴 경로의 실제 함수 부르기는 실제 모드에서만. explorer_conflict · explorer · 캡처 시험 (b3 · check) 은 기본 가짜. - windows_check_shot_test 의 "65 · 94 · 98" 은 144 뒤로 깨져 있었다 ([그대로 두기] 창을 닫지 않아 다음 크기에서 막힘) - 닫게 고침. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>묶음4
parent
5601f9d4fb
commit
f33a4b3547
|
|
@ -12,6 +12,7 @@ import 'package:jj_mkvmaker/ui/explorer_page.dart';
|
|||
import 'package:jj_mkvmaker/ui/theme.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import '../test/support/fake_recycle_bin.dart';
|
||||
import '../test/support/recycle_leftovers.dart';
|
||||
|
||||
/// 묶음 3 (데이터 쪽) 기기 감독 확인용 (Windows): 실제 앱 화면을 띄워 누르고 단계마다 PNG (JJ_SHOT_DIR) 로 남긴다.
|
||||
|
|
@ -77,6 +78,8 @@ void main() {
|
|||
..writeAsStringSync('old');
|
||||
c = AppController(PlatformServices.create());
|
||||
c.settings.explorerPaths = [left, right];
|
||||
// 휴지통: 기본은 가짜 (화면 흐름만), JJ_TEST_REAL_RECYCLE=1 이면 실제 휴지통
|
||||
useTestRecycleBin();
|
||||
});
|
||||
tearDown(() async {
|
||||
ExplorerPage.debugVolumes = null;
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ import 'package:jj_mkvmaker/ui/security_settings.dart';
|
|||
import 'package:jj_mkvmaker/ui/theme.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import '../test/support/fake_recycle_bin.dart';
|
||||
|
||||
/// 기기 감독 (Windows) 확인용: 실제 앱 화면을 띄워 누르고 단계마다 PNG (JJ_SHOT_DIR) 로 남긴다.
|
||||
/// 65 · 94 · 98 (휴지통 · 휴지통 없는 위치 · 오류 문구), 37 (경로 입력), 124 · 130 (마스터 · 최상), 123 (AI 그림).
|
||||
/// 시험 폴더 · 메모리 저장소만 쓴다 (사용자 설정 · 비밀번호는 건드리지 않음). 휴지통에 넣은 시험 항목은 끝에 $I 로 골라 지운다.
|
||||
|
|
@ -78,6 +80,8 @@ void main() {
|
|||
testWidgets('65 · 94 · 98: 지우기 - 휴지통 · 영구 삭제 · 휴지통 없는 위치 · 오류 문구', (t) async {
|
||||
final tmp = tempDir('jj_shot_del_');
|
||||
final started = DateTime.now();
|
||||
// 휴지통: 기본은 가짜 (화면 흐름만), JJ_TEST_REAL_RECYCLE=1 이면 실제 휴지통 (끝에 시험 항목만 지움)
|
||||
useTestRecycleBin();
|
||||
try {
|
||||
for (final (tag, size) in sizes) {
|
||||
final dir = Directory(p.join(tmp.path, tag))..createSync();
|
||||
|
|
@ -151,6 +155,13 @@ void main() {
|
|||
await t.tap(find.text('휴지통으로').last);
|
||||
await settle(t, 20);
|
||||
await shot(t, '98_error_text_$tag');
|
||||
// 144 뒤로는 이유 한 줄과 [그대로 두기] / [영구 삭제] 창이 뜬다 - 지우지 않고 닫아 다음 크기로
|
||||
for (final b in ['그대로 두기', '확인']) {
|
||||
if (find.text(b).evaluate().isNotEmpty) {
|
||||
await t.tap(find.text(b).last);
|
||||
await settle(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
await t.pumpWidget(const SizedBox());
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import 'dart:ffi';
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:ffi/ffi.dart';
|
||||
import 'package:flutter/foundation.dart' show visibleForTesting;
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import '../../l10n/tr.dart';
|
||||
|
|
@ -18,6 +19,26 @@ enum RecycleResult {
|
|||
cancelled,
|
||||
}
|
||||
|
||||
/// 시험용: 실제 휴지통 대신 쓸 것. 기본 전체 시험은 가짜로 흐름만 본다 - 시험이 도중에 끊기면 사용자 휴지통에
|
||||
/// 흔적이 남기 때문 (실제 휴지통 시험은 JJ_TEST_REAL_RECYCLE=1 일 때만, test/support/fake_recycle_bin.dart).
|
||||
abstract class RecycleBinBackend {
|
||||
RecycleResult move(String path);
|
||||
void restore(String original, {required DateTime since});
|
||||
}
|
||||
|
||||
/// null 이면 실제 휴지통 (앱은 늘 null)
|
||||
@visibleForTesting
|
||||
RecycleBinBackend? recycleBinForTest;
|
||||
|
||||
/// 휴지통으로 보내기 (앱의 화면은 이것을 쓴다 - 시험에서 가짜로 바꿀 수 있게)
|
||||
RecycleResult recycle(String path) => recycleBinForTest?.move(path) ?? moveToRecycleBin(path);
|
||||
|
||||
/// 휴지통에서 원래 자리로 (앱의 화면은 이것을 쓴다)
|
||||
void restoreRecycled(String original, {required DateTime since}) {
|
||||
final t = recycleBinForTest;
|
||||
t == null ? restoreFromRecycleBin(original, since: since) : t.restore(original, since: since);
|
||||
}
|
||||
|
||||
/// 이 경로가 있는 곳에 휴지통이 있는지 (94). Windows 의 휴지통은 고정 디스크에만 있다:
|
||||
/// 네트워크 드라이브 (Z:) · \\NAS\공유 · USB 메모리 · CD 는 휴지통 없이 바로 지워진다.
|
||||
bool hasRecycleBin(String path) {
|
||||
|
|
|
|||
|
|
@ -1428,7 +1428,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
for (final x in paths) {
|
||||
try {
|
||||
if (!permanent) {
|
||||
switch (moveToRecycleBin(x)) {
|
||||
switch (recycle(x)) {
|
||||
case RecycleResult.recycled:
|
||||
recycled++;
|
||||
sent.add(x);
|
||||
|
|
@ -1530,7 +1530,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
var back = 0;
|
||||
for (final x in paths) {
|
||||
try {
|
||||
restoreFromRecycleBin(x, since: since);
|
||||
restoreRecycled(x, since: since);
|
||||
back++;
|
||||
} catch (e) {
|
||||
failed.add('${vBasename(x)}: ${e is FileSystemException ? e.message : e}');
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import 'package:jj_mkvmaker/services/platform_services.dart';
|
|||
import 'package:jj_mkvmaker/ui/explorer_page.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import 'support/fake_recycle_bin.dart';
|
||||
import 'support/recycle_leftovers.dart';
|
||||
|
||||
/// 48 · 154 · 155: 같은 이름이 있으면 덮어쓰기 · 건너뛰기 · 이름 바꾸기를 고르고, 확인 창에 항목 이름이 보인다
|
||||
|
|
@ -17,6 +18,7 @@ void main() {
|
|||
late Directory tmp;
|
||||
late String left, right;
|
||||
late AppController c;
|
||||
FakeRecycleBin? bin;
|
||||
|
||||
setUp(() {
|
||||
tmp = Directory.systemTemp.createTempSync('jj_conflict_');
|
||||
|
|
@ -32,10 +34,12 @@ void main() {
|
|||
c.settings.explorerPaths = [left, right];
|
||||
final root = p.rootPrefix(tmp.path);
|
||||
ExplorerPage.debugVolumes = () => [(root, root.replaceAll(RegExp(r'[\\/]+$'), ''))];
|
||||
// 휴지통: 기본은 가짜 (흐름만), JJ_TEST_REAL_RECYCLE=1 이면 실제 휴지통
|
||||
bin = useTestRecycleBin();
|
||||
});
|
||||
tearDown(() async {
|
||||
ExplorerPage.debugVolumes = null;
|
||||
// 휴지통을 건드리는 시험 (148 · 144): 사용자 휴지통에 이 시험 폴더 아래의 흔적이 없는지 스스로 확인
|
||||
// 사용자 휴지통에 이 시험 폴더 아래의 흔적이 없는지 스스로 확인 (가짜일 때도 - 실제 휴지통에 닿지 않았는지)
|
||||
expect(await recycleLeftovers(tmp.path), 0, reason: '사용자 휴지통에 시험 흔적 없음 (하위 폴더 포함)');
|
||||
tmp.deleteSync(recursive: true);
|
||||
});
|
||||
|
|
@ -166,6 +170,7 @@ void main() {
|
|||
await settle(tester, () => find.textContaining('되돌렸습니다').evaluate().isNotEmpty);
|
||||
expect(File(p.join(left, 'doc.txt')).readAsStringSync(), 'new');
|
||||
expect(find.textContaining('1개 항목을 되돌렸습니다.'), findsOneWidget);
|
||||
if (bin != null) expect(bin!.items, isEmpty, reason: '가짜 휴지통에서 되돌림');
|
||||
// 알림 (10초) 이 시험이 끝난 뒤에 닫히지 않게 정리
|
||||
ScaffoldMessenger.of(tester.element(find.byType(ExplorerPage))).clearSnackBars();
|
||||
await tester.pumpAndSettle();
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import 'package:jj_mkvmaker/services/platform_services.dart';
|
|||
import 'package:jj_mkvmaker/ui/explorer_page.dart';
|
||||
import 'package:jj_mkvmaker/ui/path_label.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'support/fake_recycle_bin.dart';
|
||||
|
||||
/// 다른 앱으로 열기를 기록만 하는 셸 (실제로 프로그램을 띄우지 않게)
|
||||
class _Shell extends NoopShell {
|
||||
|
|
@ -29,6 +30,8 @@ void main() {
|
|||
late AppController c;
|
||||
|
||||
setUp(() {
|
||||
// 휴지통: 기본은 가짜 (이 시험들은 영구 삭제를 고르지만 혹시라도 사용자 휴지통에 닿지 않게)
|
||||
useTestRecycleBin();
|
||||
tmp = Directory.systemTemp.createTempSync('jj_explorer_');
|
||||
left = p.join(tmp.path, 'left');
|
||||
right = p.join(tmp.path, 'right');
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import 'package:flutter_test/flutter_test.dart';
|
|||
import 'package:jj_mkvmaker/platform/windows/recycle_bin.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import 'support/fake_recycle_bin.dart';
|
||||
import 'support/recycle_leftovers.dart';
|
||||
|
||||
/// 시험이 휴지통에 넣은 것만 (원래 위치가 이 시험의 임시 폴더인 항목) 휴지통에서 지운다. 사용자 항목은 건드리지 않는다.
|
||||
|
|
@ -30,7 +31,9 @@ foreach (\$it in @(\$rb.Items())) {
|
|||
return int.tryParse('${r.stdout}'.trim().split('\n').last.trim()) ?? -1;
|
||||
}
|
||||
|
||||
/// 65 · 94 · 98: Windows 휴지통 (실제 휴지통 - 시험 파일 하나 · 폴더 하나, 끝나면 휴지통에서도 지움)
|
||||
/// 65 · 94 · 98: Windows 휴지통 (실제 휴지통 - 시험 파일 하나 · 폴더 하나, 끝나면 휴지통에서도 지움).
|
||||
/// 실제 휴지통을 쓰는 시험은 JJ_TEST_REAL_RECYCLE=1 일 때만 (전체 시험 중에 끊기면 사용자 휴지통에 흔적이 남으므로).
|
||||
/// 휴지통 쪽 코드를 고칠 때 · 릴리스 전에 따로: JJ_TEST_REAL_RECYCLE=1 flutter test test/recycle_bin_test.dart
|
||||
void main() {
|
||||
test('파일 · 폴더를 휴지통으로 (실제로 들어갔는지까지) · 없는 것은 그냥 성공', () async {
|
||||
final tmp = Directory.systemTemp.createTempSync('jj_recycle_');
|
||||
|
|
@ -54,7 +57,7 @@ void main() {
|
|||
// 사용자 휴지통에 시험 항목을 남기지 않는다
|
||||
expect(await _purgeFromRecycleBin(tmp.absolute.path), 2);
|
||||
}
|
||||
}, skip: !Platform.isWindows);
|
||||
}, skip: realRecycleSkip);
|
||||
|
||||
test('148: 휴지통으로 보낸 파일 · 폴더를 원래 자리로 되돌린다 (휴지통에 남지 않음) · 원래 자리에 같은 이름이 있으면 덮지 않음', () async {
|
||||
final tmp = Directory.systemTemp.createTempSync('jj_undo_');
|
||||
|
|
@ -83,7 +86,7 @@ void main() {
|
|||
expect(recycledInfoExists(f.path, since: since), isTrue);
|
||||
// 휴지통에 없으면 실패
|
||||
expect(() => restoreFromRecycleBin(p.join(tmp.absolute.path, 'never.txt'), since: since), throwsA(isA<FileSystemException>()));
|
||||
}, skip: !Platform.isWindows);
|
||||
}, skip: realRecycleSkip);
|
||||
|
||||
test('148: 원래 폴더가 그사이 지워졌으면 다시 만들지 않고 알린다 · 이 사용자 (SID) 의 휴지통에서만 찾는다', () async {
|
||||
expect(currentUserSid(), matches(RegExp(r'^S-1-5-')));
|
||||
|
|
@ -105,7 +108,7 @@ void main() {
|
|||
.having((e) => e.message, 'message', contains('원래 폴더가 없어 되돌리지 못했습니다'))));
|
||||
expect(folder.existsSync(), isFalse, reason: '지운 폴더를 몰래 다시 만들지 않음');
|
||||
expect(recycledInfoExists(f.path, since: since), isTrue, reason: '휴지통의 것은 그대로');
|
||||
}, skip: !Platform.isWindows);
|
||||
}, skip: realRecycleSkip);
|
||||
|
||||
test('긴 경로 (260자 넘음) 는 휴지통으로 보내지 않고 이유를 알린다 - 묻지 않고 영구 삭제되던 것 (폴더 안에 있어도)', () {
|
||||
if (!Platform.isWindows) return;
|
||||
|
|
@ -122,13 +125,18 @@ void main() {
|
|||
File(lp(file)).writeAsStringSync('x');
|
||||
expect(file.length, greaterThan(259));
|
||||
expect(longPathInside(file), file);
|
||||
expect(() => moveToRecycleBin(file), throwsA(isA<FileSystemException>().having((e) => e.message, 'message', contains('경로가 너무 깁니다'))));
|
||||
expect(File(lp(file)).existsSync(), isTrue, reason: '지우지 않음');
|
||||
// 실제 휴지통 함수는 JJ_TEST_REAL_RECYCLE=1 일 때만 (고장 나면 사용자 휴지통에 들어가므로)
|
||||
if (realRecycleBin) {
|
||||
expect(() => moveToRecycleBin(file), throwsA(isA<FileSystemException>().having((e) => e.message, 'message', contains('경로가 너무 깁니다'))));
|
||||
expect(File(lp(file)).existsSync(), isTrue, reason: '지우지 않음');
|
||||
}
|
||||
// 폴더 자체는 짧아도 안에 긴 경로가 있으면
|
||||
final outer = p.join(tmp.path, 'outer');
|
||||
expect(longPathInside(outer), isNotNull);
|
||||
expect(() => moveToRecycleBin(outer), throwsA(isA<FileSystemException>()));
|
||||
expect(Directory(outer).existsSync(), isTrue);
|
||||
if (realRecycleBin) {
|
||||
expect(() => moveToRecycleBin(outer), throwsA(isA<FileSystemException>()));
|
||||
expect(Directory(outer).existsSync(), isTrue);
|
||||
}
|
||||
final short = File(p.join(tmp.path, 'short.txt'))..writeAsStringSync('x');
|
||||
expect(longPathInside(short.path), isNull, reason: '짧은 파일 경로');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,98 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:jj_mkvmaker/l10n/tr.dart';
|
||||
import 'package:jj_mkvmaker/platform/windows/recycle_bin.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
/// 실제 사용자 휴지통을 쓰는 시험은 이 값이 1 일 때만 (휴지통 쪽 코드를 고칠 때 · 릴리스 전에 따로 돌리고 흔적 0 확인).
|
||||
/// 기본 전체 시험은 [FakeRecycleBin] 으로 흐름만 본다 - 시험이 도중에 끊기면 사용자 휴지통에 흔적이 남기 때문.
|
||||
bool get realRecycleBin => Platform.environment['JJ_TEST_REAL_RECYCLE'] == '1';
|
||||
|
||||
/// 실제 휴지통 시험을 건너뛸 때의 이유 (skip 에 그대로)
|
||||
Object get realRecycleSkip => !Platform.isWindows
|
||||
? 'Windows 만'
|
||||
: realRecycleBin
|
||||
? false
|
||||
: '실제 휴지통 시험은 JJ_TEST_REAL_RECYCLE=1 일 때만';
|
||||
|
||||
/// 시험 하나 동안 휴지통을 가짜로 (JJ_TEST_REAL_RECYCLE=1 이면 실제 휴지통 그대로). 가짜 휴지통을 돌려준다 (실제면 null).
|
||||
FakeRecycleBin? useTestRecycleBin() {
|
||||
if (realRecycleBin) return null;
|
||||
final fake = FakeRecycleBin();
|
||||
recycleBinForTest = fake;
|
||||
addTearDown(() {
|
||||
recycleBinForTest = null;
|
||||
fake.dispose();
|
||||
});
|
||||
return fake;
|
||||
}
|
||||
|
||||
/// 시험용 휴지통: 시험 임시 폴더 안의 자기 폴더로 옮기고, 실제 휴지통과 같은 규칙으로 되돌린다
|
||||
/// (긴 경로는 받지 않음 · 원래 자리에 같은 이름이 있으면 덮지 않음 · 원래 폴더가 없으면 다시 만들지 않음).
|
||||
class FakeRecycleBin implements RecycleBinBackend {
|
||||
final Directory bin = Directory.systemTemp.createTempSync('jj_fake_recycle_');
|
||||
|
||||
/// 원래 경로 → 휴지통 안의 경로 (보낸 차례대로)
|
||||
final items = <String, String>{};
|
||||
var _n = 0;
|
||||
|
||||
@override
|
||||
RecycleResult move(String path) {
|
||||
if (FileSystemEntity.typeSync(path, followLinks: false) == FileSystemEntityType.notFound) return RecycleResult.recycled;
|
||||
final long = longPathInside(path);
|
||||
if (long != null) throw LongPathException(recycleErrorText(0x7C), long);
|
||||
final full = File(path).absolute.path;
|
||||
final to = p.join(bin.path, '\$R${_n++}${p.extension(full)}');
|
||||
_move(full, to);
|
||||
items[full] = to;
|
||||
return RecycleResult.recycled;
|
||||
}
|
||||
|
||||
@override
|
||||
void restore(String original, {required DateTime since}) {
|
||||
final from = items[File(original).absolute.path];
|
||||
if (from == null) throw FileSystemException(tr('휴지통에서 찾지 못했습니다 (이미 비웠거나 되돌림)'), original);
|
||||
if (FileSystemEntity.typeSync(original, followLinks: false) != FileSystemEntityType.notFound) {
|
||||
throw FileSystemException(tr('원래 자리에 같은 이름이 이미 있습니다'), original);
|
||||
}
|
||||
if (!Directory(p.dirname(original)).existsSync()) {
|
||||
throw FileSystemException(tr('원래 폴더가 없어 되돌리지 못했습니다 (휴지통에서 직접 복원하세요)'), original);
|
||||
}
|
||||
_move(from, original);
|
||||
items.remove(File(original).absolute.path);
|
||||
}
|
||||
|
||||
/// 옮기기 (다른 드라이브면 복사한 뒤 지운다 - 시험 폴더가 M: 이고 가짜 휴지통이 C: 의 임시 폴더일 때)
|
||||
static void _move(String from, String to) {
|
||||
final dir = FileSystemEntity.isDirectorySync(from);
|
||||
try {
|
||||
dir ? Directory(from).renameSync(to) : File(from).renameSync(to);
|
||||
return;
|
||||
} on FileSystemException {
|
||||
// 아래에서 복사
|
||||
}
|
||||
if (!dir) {
|
||||
File(from).copySync(to);
|
||||
File(from).deleteSync();
|
||||
return;
|
||||
}
|
||||
Directory(to).createSync(recursive: true);
|
||||
for (final e in Directory(from).listSync(recursive: true, followLinks: false)) {
|
||||
final dst = p.join(to, p.relative(e.path, from: from));
|
||||
if (e is Directory) {
|
||||
Directory(dst).createSync(recursive: true);
|
||||
} else if (e is File) {
|
||||
File(dst).parent.createSync(recursive: true);
|
||||
e.copySync(dst);
|
||||
}
|
||||
}
|
||||
Directory(from).deleteSync(recursive: true);
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
try {
|
||||
bin.deleteSync(recursive: true);
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue