158 lines
6.4 KiB
Dart
158 lines
6.4 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));
|
|
|
|
Future<void> idle(FolderSizes s, String path) async {
|
|
for (var i = 0; i < 300 && s.of(path) == null; i++) {
|
|
await Future<void>.delayed(const Duration(milliseconds: 10));
|
|
}
|
|
}
|
|
|
|
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 Future<void>.delayed(const Duration(milliseconds: 20));
|
|
expect(s.busy(a) && s.busy(b), isTrue);
|
|
// 다른 폴더로 감
|
|
s.retainAuto([p.join(tmp.path, 'elsewhere')]);
|
|
expect(s.busy(b), isFalse, reason: '기다리던 것은 빠진다');
|
|
gate.complete();
|
|
await Future<void>.delayed(const Duration(milliseconds: 100));
|
|
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 Future<void>.delayed(const Duration(milliseconds: 100));
|
|
expect(s.of(a), isNull);
|
|
expect(s.busy(a), isFalse);
|
|
s.dispose();
|
|
});
|
|
|
|
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']);
|
|
});
|
|
}
|