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 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.delayed(const Duration(milliseconds: 10)); } } Future 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.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 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(); final read = []; 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']); }); }