JJ_MKVMaker/test/folder_size_test.dart

163 lines
6.8 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: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']);
});
}