WIP HWP 2단계 (관리자 지시 10/20): 열어 두는 PDF 묶음은 최근 3개까지 (몇백 쪽도 메모리가 늘지 않게, 그리는 묶음은 닫지 않음, 닫은 묶음은 파일만 다시 열기) · 보기 화면의 "열 수 없습니다" 에 [다시 시도] (그 장의 기억을 지우고 다시 부름 - 모든 보기 원본) · 닫을 때 만드는 중인 묶음을 기다리지 않음 · 묶음 여는 부분을 바꿔 끼울 수 있게 (HwpChunkOpener) 해서 단위 시험 hwp_source_unit_test (PDFium 없이 4개) (검토 대기)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>hwp2
parent
65098287e8
commit
34efa34193
|
|
@ -34,77 +34,31 @@ class HwpPdfException implements Exception {
|
|||
String toString() => message;
|
||||
}
|
||||
|
||||
/// 관리자 지시 (10/20): HWP · HWPX 를 보기 화면으로 - 큰 문서 (편람 383쪽은 PC 에서도 14분) 를 통째로 바꾸지 않고,
|
||||
/// 보는 쪽이 든 [chunk] 쪽 묶음만 PDF 로 바꿔 보이면서 다음 묶음을 뒤에서 미리 만든다. 묶음마다 [timeout],
|
||||
/// 닫으면 (dispose) 더 만들지 않는다. 바꾸는 중의 쪽은 보기 화면이 기다리는 표시를 보이고, 실패한 묶음은 그 쪽에 이유가 보인다.
|
||||
class HwpSource extends ReaderSource {
|
||||
final String path;
|
||||
final HwpPdfDoc doc;
|
||||
final Directory work;
|
||||
final List<String> fontDirs;
|
||||
final int chunk;
|
||||
final Duration timeout;
|
||||
@override
|
||||
final String title;
|
||||
/// 바꾼 PDF 한 묶음 (몇 쪽). 기본은 pdfrx ([pdfChunkOpener]) - 시험은 PDFium 없이 가짜로
|
||||
abstract class HwpChunk {
|
||||
int get pageCount;
|
||||
Future<ReaderImage> render(int index, {required int maxWidth});
|
||||
Future<void> dispose();
|
||||
}
|
||||
|
||||
final _chunks = <int, Future<PdfDocument>>{};
|
||||
final _opened = <PdfDocument>[];
|
||||
var _disposed = false;
|
||||
/// PDF 파일을 묶음으로 연다
|
||||
typedef HwpChunkOpener = Future<HwpChunk> Function(String pdfPath);
|
||||
|
||||
HwpSource._(this.path, this.doc, this.work, this.title, {required this.fontDirs, required this.chunk, required this.timeout});
|
||||
|
||||
/// 문서를 연다 (WebDAV 등은 [tempDir] 로 받은 뒤). 열지 못하면 [HwpPdfException]
|
||||
static Future<HwpSource> open(String path,
|
||||
{required HwpPdfEngine engine,
|
||||
required String tempDir,
|
||||
String? password,
|
||||
List<String> fontDirs = const [],
|
||||
int chunk = 5,
|
||||
Duration timeout = const Duration(minutes: 3)}) async {
|
||||
final local = await vLocalCopy(path, tempDir);
|
||||
final doc = await engine.open(local, password: password);
|
||||
final work = Directory(p.join(tempDir, 'jj_hwp_${DateTime.now().microsecondsSinceEpoch}'))..createSync(recursive: true);
|
||||
return HwpSource._(path, doc, work, vBasename(path), fontDirs: fontDirs, chunk: chunk, timeout: timeout);
|
||||
}
|
||||
|
||||
@override
|
||||
int get length => doc.pageCount;
|
||||
@override
|
||||
String pageName(int i) => '${i + 1} / $length';
|
||||
|
||||
/// [k] 번째 묶음의 PDF (한 번만 만든다 - 실패하면 다음에 다시)
|
||||
Future<PdfDocument> _chunk(int k) => _chunks[k] ??= _make(k).onError<Object>((e, st) {
|
||||
_chunks.remove(k);
|
||||
Error.throwWithStackTrace(e, st);
|
||||
});
|
||||
|
||||
Future<PdfDocument> _make(int k) async {
|
||||
if (_disposed) throw StateError(tr('닫힌 문서입니다'));
|
||||
final from = k * chunk, to = math.min(from + chunk, length);
|
||||
final out = p.join(work.path, 'p$k.pdf');
|
||||
try {
|
||||
await doc.exportPdf(from, to, out, fontDirs: fontDirs).timeout(timeout);
|
||||
} on TimeoutException {
|
||||
throw HwpPdfException(trf('{0}~{1}쪽을 {2}초 안에 바꾸지 못했습니다', [from + 1, to, timeout.inSeconds]));
|
||||
}
|
||||
if (_disposed) throw StateError(tr('닫힌 문서입니다'));
|
||||
/// 기본: pdfrx 로 연다
|
||||
Future<HwpChunk> pdfChunkOpener(String pdfPath) async {
|
||||
await pdfrxFlutterInitialize();
|
||||
final pdf = await PdfDocument.openFile(out);
|
||||
if (_disposed) {
|
||||
await pdf.dispose();
|
||||
throw StateError(tr('닫힌 문서입니다'));
|
||||
}
|
||||
_opened.add(pdf);
|
||||
return pdf;
|
||||
}
|
||||
return _PdfChunk(await PdfDocument.openFile(pdfPath));
|
||||
}
|
||||
|
||||
class _PdfChunk implements HwpChunk {
|
||||
final PdfDocument doc;
|
||||
_PdfChunk(this.doc);
|
||||
@override
|
||||
int get pageCount => doc.pages.length;
|
||||
|
||||
@override
|
||||
Future<ReaderImage> load(int i, {required int maxWidth}) async {
|
||||
final k = i ~/ chunk;
|
||||
final pdf = await _chunk(k);
|
||||
// 다음 묶음을 미리 (넘겨 볼 때 기다리지 않게) - 실패해도 여기서는 알리지 않는다 (그 쪽을 볼 때 보임)
|
||||
if ((k + 1) * chunk < length) unawaited(_chunk(k + 1).then((_) {}, onError: (Object _) {}));
|
||||
final page = pdf.pages[i - k * chunk];
|
||||
Future<ReaderImage> render(int index, {required int maxWidth}) async {
|
||||
final page = doc.pages[index];
|
||||
final w = maxWidth.toDouble();
|
||||
final h = w * page.height / page.width;
|
||||
final image = await page.render(fullWidth: w, fullHeight: h, backgroundColor: 0xffffffff);
|
||||
|
|
@ -116,13 +70,149 @@ class HwpSource extends ReaderSource {
|
|||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> dispose() => doc.dispose();
|
||||
}
|
||||
|
||||
/// 관리자 지시 (10/20): HWP · HWPX 를 보기 화면으로 - 큰 문서 (편람 383쪽은 PC 에서도 14분) 를 통째로 바꾸지 않고,
|
||||
/// 보는 쪽이 든 [chunk] 쪽 묶음만 PDF 로 바꿔 보이면서 다음 묶음을 뒤에서 미리 만든다. 묶음마다 [timeout],
|
||||
/// 닫으면 (dispose) 더 만들지 않는다. 실패한 묶음은 그 쪽에 이유가 보이고 [다시 시도] 로 다시 만든다 (보기 화면).
|
||||
/// 2단계 (10/20): 메모리 - 열어 둔 PDF 묶음은 최근 것 [maxOpenChunks] 개까지 (몇백 쪽 문서도 늘지 않게). 닫은 묶음의 파일은
|
||||
/// 남겨 다시 볼 때 바꾸지 않고 파일만 연다. 그리고 있는 묶음은 닫지 않는다.
|
||||
class HwpSource extends ReaderSource {
|
||||
final String path;
|
||||
final HwpPdfDoc doc;
|
||||
final Directory work;
|
||||
final List<String> fontDirs;
|
||||
final int chunk;
|
||||
final Duration timeout;
|
||||
final int maxOpenChunks;
|
||||
final HwpChunkOpener opener;
|
||||
@override
|
||||
final String title;
|
||||
|
||||
/// 열어 둔 묶음 (넣은 순서 = 최근에 쓴 순서, 앞이 오래된 것)
|
||||
final _open = <int, Future<HwpChunk>>{};
|
||||
|
||||
/// PDF 파일을 다 만든 묶음
|
||||
final _made = <int>{};
|
||||
|
||||
/// 지금 그리고 있는 묶음 (닫지 않는다)
|
||||
final _busy = <int, int>{};
|
||||
var _disposed = false;
|
||||
|
||||
HwpSource._(this.path, this.doc, this.work, this.title,
|
||||
{required this.fontDirs, required this.chunk, required this.timeout, required this.maxOpenChunks, required this.opener});
|
||||
|
||||
/// 문서를 연다 (WebDAV 등은 [tempDir] 로 받은 뒤). 열지 못하면 [HwpPdfException]
|
||||
static Future<HwpSource> open(String path,
|
||||
{required HwpPdfEngine engine,
|
||||
required String tempDir,
|
||||
String? password,
|
||||
List<String> fontDirs = const [],
|
||||
int chunk = 5,
|
||||
Duration timeout = const Duration(minutes: 3),
|
||||
int maxOpenChunks = 3,
|
||||
HwpChunkOpener opener = pdfChunkOpener}) async {
|
||||
final local = await vLocalCopy(path, tempDir);
|
||||
final doc = await engine.open(local, password: password);
|
||||
final work = Directory(p.join(tempDir, 'jj_hwp_${DateTime.now().microsecondsSinceEpoch}'))..createSync(recursive: true);
|
||||
return HwpSource._(path, doc, work, vBasename(path),
|
||||
fontDirs: fontDirs, chunk: chunk, timeout: timeout, maxOpenChunks: math.max(2, maxOpenChunks), opener: opener);
|
||||
}
|
||||
|
||||
@override
|
||||
int get length => doc.pageCount;
|
||||
@override
|
||||
String pageName(int i) => '${i + 1} / $length';
|
||||
|
||||
/// 열어 둔 묶음 번호 (오래된 것부터 - 시험용)
|
||||
List<int> get openChunks => _open.keys.toList();
|
||||
|
||||
/// PDF 파일을 만든 묶음 번호 (시험용)
|
||||
Set<int> get madeChunks => Set.unmodifiable(_made);
|
||||
|
||||
/// [k] 번째 묶음 (실패하면 지워서 다음에 다시)
|
||||
Future<HwpChunk> _chunk(int k) {
|
||||
final had = _open.remove(k);
|
||||
if (had != null) {
|
||||
_open[k] = had; // 최근에 쓴 것으로
|
||||
return had;
|
||||
}
|
||||
late final Future<HwpChunk> f;
|
||||
f = _openChunk(k).onError<Object>((e, st) {
|
||||
if (identical(_open[k], f)) _open.remove(k);
|
||||
Error.throwWithStackTrace(e, st);
|
||||
});
|
||||
_open[k] = f;
|
||||
_evict();
|
||||
return f;
|
||||
}
|
||||
|
||||
Future<HwpChunk> _openChunk(int k) async {
|
||||
if (_disposed) throw StateError(tr('닫힌 문서입니다'));
|
||||
final out = p.join(work.path, 'p$k.pdf');
|
||||
if (!_made.contains(k) || !File(out).existsSync()) {
|
||||
final from = k * chunk, to = math.min(from + chunk, length);
|
||||
try {
|
||||
await doc.exportPdf(from, to, out, fontDirs: fontDirs).timeout(timeout);
|
||||
} on TimeoutException {
|
||||
throw HwpPdfException(trf('{0}~{1}쪽을 {2}초 안에 바꾸지 못했습니다', [from + 1, to, timeout.inSeconds]));
|
||||
} catch (_) {
|
||||
// 닫은 뒤에 끝난 변환은 (임시 폴더를 이미 지워) 실패한다 - 그 이유 대신 "닫힌 문서"
|
||||
if (_disposed) throw StateError(tr('닫힌 문서입니다'));
|
||||
rethrow;
|
||||
}
|
||||
_made.add(k);
|
||||
}
|
||||
if (_disposed) throw StateError(tr('닫힌 문서입니다'));
|
||||
final c = await opener(out);
|
||||
if (_disposed) {
|
||||
await c.dispose();
|
||||
throw StateError(tr('닫힌 문서입니다'));
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
/// 열어 둔 묶음이 [maxOpenChunks] 를 넘으면 오래된 것부터 닫는다 (그리고 있는 것은 빼고)
|
||||
void _evict() {
|
||||
while (_open.length > maxOpenChunks) {
|
||||
final victim = _open.keys.where((k) => (_busy[k] ?? 0) == 0).firstOrNull;
|
||||
if (victim == null) return;
|
||||
final f = _open.remove(victim)!;
|
||||
unawaited(f.then((c) => c.dispose(), onError: (Object _) {}));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ReaderImage> load(int i, {required int maxWidth}) async {
|
||||
final k = i ~/ chunk;
|
||||
_busy[k] = (_busy[k] ?? 0) + 1;
|
||||
try {
|
||||
final c = await _chunk(k);
|
||||
// 다음 묶음을 미리 (넘겨 볼 때 기다리지 않게) - 실패해도 여기서는 알리지 않는다 (그 쪽을 볼 때 보임)
|
||||
if ((k + 1) * chunk < length) unawaited(_chunk(k + 1).then((_) {}, onError: (Object _) {}));
|
||||
return await c.render(i - k * chunk, maxWidth: maxWidth);
|
||||
} finally {
|
||||
final n = (_busy[k] ?? 1) - 1;
|
||||
if (n <= 0) {
|
||||
_busy.remove(k);
|
||||
} else {
|
||||
_busy[k] = n;
|
||||
}
|
||||
if (!_disposed) _evict();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
_disposed = true;
|
||||
for (final d in _opened) {
|
||||
await d.dispose();
|
||||
final open = [..._open.values];
|
||||
_open.clear();
|
||||
// 만드는 중인 묶음은 기다리지 않는다 (몇 분 걸릴 수 있음) - 끝나면 닫힌다 ([_openChunk] 가 _disposed 를 본다)
|
||||
for (final f in open) {
|
||||
unawaited(f.then((c) => c.dispose(), onError: (Object _) {}));
|
||||
}
|
||||
_opened.clear();
|
||||
await doc.close(); // 바꾸는 중이면 그것이 끝난 뒤에 닫힌다 (엔진이 한 번에 하나씩)
|
||||
try {
|
||||
if (work.existsSync()) work.deleteSync(recursive: true);
|
||||
|
|
|
|||
|
|
@ -441,10 +441,23 @@ class _ReaderPageState extends State<ReaderPage> {
|
|||
final fit = c.settings.readerFit;
|
||||
final slot = _v[i];
|
||||
final half = slot.part >= 0;
|
||||
// 관리자 지시 (10/20, HWP 2단계): 이유와 [다시 시도] - 그 장의 기억을 지우고 다시 부른다 (시간 제한을 넘긴 HWP 묶음 등)
|
||||
Widget failed(Object? e) => SizedBox(
|
||||
width: 320,
|
||||
height: 120,
|
||||
child: Center(child: Text(trf('열 수 없습니다: {0}', [e]), style: const TextStyle(color: Colors.white70))));
|
||||
height: 160,
|
||||
child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||
Text(trf('열 수 없습니다: {0}', [e]), textAlign: TextAlign.center, style: const TextStyle(color: Colors.white70)),
|
||||
const SizedBox(height: 8),
|
||||
OutlinedButton(
|
||||
onPressed: () => setState(() {
|
||||
_cache.remove(slot.page);
|
||||
_cacheHalf.remove(slot.page);
|
||||
_sharp.remove(i);
|
||||
_sharpWidth.remove(i);
|
||||
}),
|
||||
child: Text(tr('다시 시도')),
|
||||
),
|
||||
]));
|
||||
// 확대해 다시 그린 것이 있으면 그것 (다 그릴 때까지는 앞의 그림을 그대로 보인다)
|
||||
final page = FutureBuilder<ReaderImage>(
|
||||
key: ValueKey('$_version:${slot.page}:${slot.part}'),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,169 @@
|
|||
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 를 열지 않음');
|
||||
});
|
||||
}
|
||||
Loading…
Reference in New Issue