JJ_MKVMaker/test/hwp_source_unit_test.dart

185 lines
7.4 KiB
Dart

import 'dart:async';
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:jj_mkvmaker/core/hwp_source.dart';
import 'package:jj_mkvmaker/core/reader_sources.dart';
import 'package:path/path.dart' as p;
/// 쪽 범위를 글로 적은 파일을 "PDF" 로 만드는 가짜 엔진 (rhwp 대신) - 부른 범위를 적어 둔다
class _Engine implements HwpPdfEngine {
final int pages;
final calls = <(int, int)>[];
Completer<void>? hold;
var closed = false;
_Engine(this.pages);
@override
Future<HwpPdfDoc> open(String path, {String? password}) async => _Doc(this);
}
class _Doc implements HwpPdfDoc {
final _Engine e;
_Doc(this.e);
@override
int get pageCount => e.pages;
@override
Future<void> exportPdf(int from, int to, String out, {List<String> fontDirs = const []}) async {
e.calls.add((from, to));
final h = e.hold;
if (h != null) await h.future;
File(out).writeAsStringSync('$from-$to');
}
@override
Future<void> close() async => e.closed = true;
}
/// 파일을 읽어 쪽 수를 아는 가짜 묶음 (PDFium 없이) - 연 것 · 닫은 것을 적어 둔다
class _Opener {
final opened = <String>[];
final disposed = <String>[];
Completer<void>? renderHold;
Future<HwpChunk> call(String path) async {
opened.add(p.basename(path));
final parts = File(path).readAsStringSync().split('-').map(int.parse).toList();
return _Chunk(this, p.basename(path), parts[1] - parts[0]);
}
}
class _Chunk implements HwpChunk {
final _Opener o;
final String name;
@override
final int pageCount;
_Chunk(this.o, this.name, this.pageCount);
@override
Future<ReaderImage> render(int index, {required int maxWidth}) async {
expect(index, lessThan(pageCount));
final h = o.renderHold;
if (h != null) await h.future;
return const ReaderImage();
}
@override
Future<void> dispose() async => o.disposed.add(name);
}
/// 관리자 지시 (10/20, HWP 2단계): 보기 원본 HwpSource 의 흐름 - 묶음 나누기 · 미리 만들기 · 열어 두는 묶음 상한 (메모리) ·
/// 그리는 묶음은 닫지 않음 · 닫은 묶음은 다시 바꾸지 않고 파일만 · 시간 제한 → 이유 · 다시 시도 · 닫기
void main() {
late Directory dir;
late File doc;
setUp(() {
dir = Directory.systemTemp.createTempSync('jj_hwpunit_');
doc = File(p.join(dir.path, 'a.hwp'))..writeAsStringSync('x');
});
tearDown(() {
if (dir.existsSync()) dir.deleteSync(recursive: true);
});
Future<void> settle() => Future<void>.delayed(const Duration(milliseconds: 50));
test('보는 쪽의 묶음 + 다음 묶음만 · 열어 둔 묶음은 3개까지 (오래된 것부터 닫음) · 다시 보면 파일만 연다', () async {
final e = _Engine(40), o = _Opener();
final s = await HwpSource.open(doc.path, engine: e, tempDir: dir.path, chunk: 5, maxOpenChunks: 3, opener: o.call);
expect(s.length, 40);
await s.load(0, maxWidth: 100);
await settle();
expect(e.calls, [(0, 5), (5, 10)]);
expect(s.openChunks, [0, 1]);
await s.load(12, maxWidth: 100); // 묶음 2 + 미리 3 → 4개 → 0 을 닫음
await settle();
expect(e.calls, [(0, 5), (5, 10), (10, 15), (15, 20)]);
expect(s.openChunks, [1, 2, 3]);
expect(o.disposed, ['p0.pdf']);
// 닫은 묶음 0 을 다시: 바꾸지 않고 (calls 그대로) 파일만 연다
await s.load(1, maxWidth: 100);
await settle();
expect(e.calls.where((c) => c == (0, 5)), hasLength(1));
expect(o.opened.where((n) => n == 'p0.pdf'), hasLength(2));
expect(s.openChunks.length, lessThanOrEqualTo(3));
expect(s.madeChunks, containsAll([0, 1, 2, 3]));
// 끝 묶음은 남은 쪽만
await s.load(39, maxWidth: 100);
expect(e.calls.last, (35, 40));
await s.dispose();
expect(e.closed, isTrue);
expect(s.work.existsSync(), isFalse, reason: '닫으면 만든 PDF 들을 지움');
});
test('그리고 있는 묶음은 상한을 넘어도 닫지 않고, 다 그린 뒤에 닫는다', () async {
final e = _Engine(40), o = _Opener();
final s = await HwpSource.open(doc.path, engine: e, tempDir: dir.path, chunk: 5, maxOpenChunks: 2, opener: o.call);
final hold = Completer<void>();
o.renderHold = hold;
final drawing = s.load(2, maxWidth: 100); // 묶음 0 을 그리는 중
await settle();
o.renderHold = null;
await s.load(20, maxWidth: 100); // 묶음 4 (+ 5)
await s.load(30, maxWidth: 100); // 묶음 6 (+ 7)
await settle();
expect(o.disposed, isNot(contains('p0.pdf')), reason: '그리는 중');
expect(s.openChunks, contains(0));
hold.complete();
await drawing;
await s.load(35, maxWidth: 100);
await settle();
expect(o.disposed, contains('p0.pdf'), reason: '다 그린 뒤에는 상한에 맞춰 닫힘');
expect(s.openChunks.length, lessThanOrEqualTo(2));
await s.dispose();
});
test('시간 제한을 넘기면 쪽 범위 · 초가 든 이유, 다시 부르면 다시 만든다', () async {
final e = _Engine(8)..hold = Completer<void>();
final o = _Opener();
final s = await HwpSource.open(doc.path, engine: e, tempDir: dir.path, chunk: 5, timeout: const Duration(milliseconds: 200),
opener: o.call);
Object? err;
try {
await s.load(0, maxWidth: 100);
} catch (x) {
err = x;
}
expect(err, isA<HwpPdfException>());
expect('$err', allOf(contains('1~5'), contains('0초')));
expect(s.openChunks, isNot(contains(0)), reason: '실패한 묶음은 기억하지 않음 - [다시 시도] 가 다시 만든다');
e.hold!.complete();
e.hold = null;
await s.load(0, maxWidth: 100);
expect(e.calls.where((c) => c == (0, 5)).length, greaterThanOrEqualTo(2));
await s.dispose();
});
test('닫으면 만드는 중인 묶음은 끝난 뒤 스스로 닫히고 더 만들지 않는다', () async {
final e = _Engine(10)..hold = Completer<void>();
final o = _Opener();
final s = await HwpSource.open(doc.path, engine: e, tempDir: dir.path, chunk: 5, opener: o.call);
final pending = s.load(0, maxWidth: 100).then<Object?>((_) => null, onError: (Object x) => x);
await settle();
await s.dispose(); // 기다리지 않고 바로 끝난다
e.hold!.complete();
final r = await pending;
expect(r, isA<StateError>(), reason: '닫힌 문서');
expect(o.opened, isEmpty, reason: '닫은 뒤에는 PDF 를 열지 않음');
});
test('앱이 켜질 때 남은 HWP 작업 폴더 (jj_hwp_) 만 지운다 - 막 생긴 것 · 다른 이름은 남김', () {
final old = Directory(p.join(dir.path, 'jj_hwp_1'))..createSync();
File(p.join(old.path, 'p0.pdf')).writeAsStringSync('x');
final fresh = Directory(p.join(dir.path, 'jj_hwp_2'))..createSync();
final other = Directory(p.join(dir.path, 'jj_hwpx_keep'))..createSync();
final file = File(p.join(dir.path, 'jj_hwp_file'))..writeAsStringSync('x');
// 5분 뒤로 보면 old · fresh 둘 다 1분이 지났다 - fresh 는 지금 기준으로는 남는다
expect(removeStaleHwpWork(dir.path), isEmpty, reason: '모두 막 생김');
final gone = removeStaleHwpWork(dir.path, now: DateTime.now().add(const Duration(minutes: 5)));
expect(gone..sort(), ['jj_hwp_1', 'jj_hwp_2']);
expect(old.existsSync() || fresh.existsSync(), isFalse);
expect(other.existsSync(), isTrue, reason: '이름이 jj_hwp_ 로 시작하지 않음 (jj_hwpx_…)');
expect(file.existsSync(), isTrue, reason: '폴더만');
});
}