163 lines
6.8 KiB
Dart
163 lines
6.8 KiB
Dart
import 'dart:async';
|
||
import 'dart:io';
|
||
|
||
import 'package:flutter_test/flutter_test.dart';
|
||
import 'package:jj_mkvmaker/core/file_ops.dart';
|
||
import 'package:jj_mkvmaker/core/folder_size.dart';
|
||
import 'package:path/path.dart' as p;
|
||
|
||
/// 80: 폴더 크기 - 필요할 때 계산 · 기억, 읽지 못한 곳은 "+", 바뀌면 그 폴더와 위 폴더를 잊음, 크기순 정렬.
|
||
/// 80 보충: 링크 · junction 은 따라가지 않음 (고리도 끝남), 화면에서 사라진 자동 계산은 멈춤, 사용자가 시킨 것은 [cancel] 로만
|
||
void main() {
|
||
late Directory tmp;
|
||
setUp(() {
|
||
tmp = Directory.systemTemp.createTempSync('jj_fsize80_');
|
||
File(p.join(tmp.path, 'a', 'x.bin'))
|
||
..createSync(recursive: true)
|
||
..writeAsBytesSync(List.filled(1000, 0));
|
||
File(p.join(tmp.path, 'a', 'deep', 'y.bin'))
|
||
..createSync(recursive: true)
|
||
..writeAsBytesSync(List.filled(24, 0));
|
||
File(p.join(tmp.path, 'b', 'z.bin'))
|
||
..createSync(recursive: true)
|
||
..writeAsBytesSync(List.filled(5, 0));
|
||
});
|
||
tearDown(() => tmp.deleteSync(recursive: true));
|
||
|
||
// 관리자 지적 (10/18): 전체 시험 중 PC 가 바쁘면 정해진 횟수 (300 × 10ms) 안에 끝나지 않아 흔들렸다 - 실제 시간으로 (최대 30초) 기다린다
|
||
Future<void> until(bool Function() ok, {Duration max = const Duration(seconds: 30)}) async {
|
||
final end = DateTime.now().add(max);
|
||
while (!ok() && DateTime.now().isBefore(end)) {
|
||
await Future<void>.delayed(const Duration(milliseconds: 10));
|
||
}
|
||
}
|
||
|
||
Future<void> idle(FolderSizes s, String path) => until(() => s.of(path) != null);
|
||
|
||
test('안쪽 폴더까지 합 · 기억 · 바뀌면 그 폴더 · 안쪽 · 위 폴더를 잊음', () async {
|
||
var reads = 0;
|
||
final s = FolderSizes(list: (d) {
|
||
reads++;
|
||
return FolderSizes.listForSize(d);
|
||
});
|
||
final a = p.join(tmp.path, 'a');
|
||
s.request(a);
|
||
expect(s.busy(a), isTrue);
|
||
await idle(s, a);
|
||
expect(s.of(a)!.bytes, 1024);
|
||
expect(s.of(a)!.partial, isFalse);
|
||
final first = reads;
|
||
s.request(a);
|
||
await Future<void>.delayed(const Duration(milliseconds: 20));
|
||
expect(reads, first, reason: '아는 것은 다시 읽지 않는다');
|
||
|
||
// 안쪽 폴더 크기만 잊어도 위 폴더 (a) 도 잊는다
|
||
s.request(p.join(a, 'deep'));
|
||
await idle(s, p.join(a, 'deep'));
|
||
s.forget(p.join(a, 'deep'));
|
||
expect(s.of(a), isNull);
|
||
expect(s.of(p.join(a, 'deep')), isNull);
|
||
|
||
// 위 폴더를 잊으면 (새로 고침) 안쪽 폴더도
|
||
s.request(p.join(a, 'deep'));
|
||
await idle(s, p.join(a, 'deep'));
|
||
s.forget(tmp.path);
|
||
expect(s.of(p.join(a, 'deep')), isNull);
|
||
s.dispose();
|
||
});
|
||
|
||
test('읽지 못한 폴더가 있으면 "+" (실제보다 작을 수 있음)', () async {
|
||
final a = p.join(tmp.path, 'a');
|
||
final s = FolderSizes(list: (d) {
|
||
if (p.basename(d) == 'deep') throw const FileSystemException('권한 없음');
|
||
return FolderSizes.listForSize(d);
|
||
});
|
||
s.request(a);
|
||
await idle(s, a);
|
||
expect(s.of(a)!.bytes, 1000);
|
||
expect(s.of(a)!.label, '1000B+');
|
||
s.dispose();
|
||
});
|
||
|
||
group('링크 · junction (Windows)', () {
|
||
Future<bool> junction(String link, String target) async =>
|
||
(await Process.run('cmd', ['/c', 'mklink', '/J', link, target])).exitCode == 0;
|
||
|
||
test('junction · 심볼릭 링크는 따라가지 않고 0 · "링크 n개 제외", 위 폴더를 가리키는 고리도 끝난다', () async {
|
||
final a = p.join(tmp.path, 'a');
|
||
// b (5B) 를 가리키는 junction, 위 폴더 (tmp) 를 가리키는 고리 junction
|
||
expect(await junction(p.join(a, 'to_b'), p.join(tmp.path, 'b')), isTrue);
|
||
expect(await junction(p.join(a, 'deep', 'loop'), tmp.path), isTrue);
|
||
var symlink = false;
|
||
try {
|
||
Link(p.join(a, 'sym_b')).createSync(p.join(tmp.path, 'b'));
|
||
symlink = true;
|
||
} catch (_) {} // 개발자 모드가 아니면 심볼릭 링크를 만들 수 없다 - junction 만 본다
|
||
final s = FolderSizes();
|
||
s.request(a);
|
||
await idle(s, a);
|
||
expect(s.of(a)!.bytes, 1024, reason: '링크가 가리키는 b (5B) 를 더하지 않는다');
|
||
expect(s.of(a)!.links, symlink ? 3 : 2);
|
||
expect(s.of(a)!.label, '1KB · 링크 ${symlink ? 3 : 2}개 제외');
|
||
s.dispose();
|
||
// tearDown 의 지우기가 junction 을 따라가 b · tmp 를 지우지 않게 링크부터 지운다
|
||
for (final l in [p.join(a, 'to_b'), p.join(a, 'deep', 'loop'), if (symlink) p.join(a, 'sym_b')]) {
|
||
Link(l).deleteSync();
|
||
}
|
||
expect(File(p.join(tmp.path, 'b', 'z.bin')).existsSync(), isTrue);
|
||
});
|
||
}, skip: !Platform.isWindows);
|
||
|
||
test('화면에서 사라진 폴더의 자동 계산은 멈추고 빠진다 · 사용자가 시킨 것은 남는다 · [cancel]', () async {
|
||
final gate = Completer<void>();
|
||
final read = <String>[];
|
||
final s = FolderSizes(list: (d) async {
|
||
read.add(p.basename(d));
|
||
await gate.future;
|
||
return FolderSizes.listForSize(d);
|
||
});
|
||
final a = p.join(tmp.path, 'a'), b = p.join(tmp.path, 'b');
|
||
s.request(a, forDir: tmp.path); // 세는 중 (gate 에서 기다림)
|
||
s.request(b, forDir: tmp.path); // 기다리는 중
|
||
await until(() => read.contains('a'));
|
||
expect(s.busy(a) && s.busy(b), isTrue);
|
||
// 다른 폴더로 감
|
||
s.retainAuto([p.join(tmp.path, 'elsewhere')]);
|
||
expect(s.busy(b), isFalse, reason: '기다리던 것은 빠진다');
|
||
gate.complete();
|
||
await until(() => !s.busy(a));
|
||
expect(s.busy(a), isFalse);
|
||
expect(s.of(a), isNull, reason: '세던 것은 멈추고 남기지 않는다');
|
||
expect(read, isNot(contains('b')));
|
||
|
||
// 사용자가 시킨 것은 화면이 바뀌어도 끝까지
|
||
s.request(b);
|
||
s.retainAuto([p.join(tmp.path, 'elsewhere')]);
|
||
await idle(s, b);
|
||
expect(s.of(b)!.bytes, 5);
|
||
|
||
// [cancel]
|
||
s.request(a);
|
||
s.cancel(a);
|
||
await until(() => !s.busy(a));
|
||
expect(s.of(a), isNull);
|
||
expect(s.busy(a), isFalse);
|
||
s.dispose();
|
||
}, timeout: const Timeout(Duration(minutes: 2)));
|
||
|
||
test('크기순 정렬: 아는 폴더는 그 크기로, 모르는 폴더는 가장 작게 (폴더는 늘 위)', () {
|
||
final now = DateTime(2026);
|
||
final list = [
|
||
FileEntry('/r/f.bin', isDir: false, size: 10, modified: now),
|
||
FileEntry('/r/small', isDir: true, modified: now),
|
||
FileEntry('/r/big', isDir: true, modified: now),
|
||
FileEntry('/r/unknown', isDir: true, modified: now),
|
||
];
|
||
final sizes = {'/r/small': 5, '/r/big': 5000};
|
||
final up = sortEntries(list, SortBy.size, dirSize: (d) => sizes[d.path]);
|
||
expect(up.map((e) => e.name), ['unknown', 'small', 'big', 'f.bin']);
|
||
final down = sortEntries(list, SortBy.size, descending: true, dirSize: (d) => sizes[d.path]);
|
||
expect(down.map((e) => e.name), ['big', 'small', 'unknown', 'f.bin']);
|
||
});
|
||
}
|