JJ_MKVMaker/test/sqlite_reader_test.dart

143 lines
7.5 KiB
Dart

import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:jj_mkvmaker/app/app_controller.dart';
import 'package:jj_mkvmaker/core/sqlite_reader.dart';
import 'package:jj_mkvmaker/platform/windows/desktop_storage_service.dart';
import 'package:jj_mkvmaker/platform/windows/process_media_tool.dart';
import 'package:jj_mkvmaker/services/platform_services.dart';
import 'package:jj_mkvmaker/ui/sqlite_viewer.dart';
import 'package:path/path.dart' as p;
/// X-plore 비교 5-1 ④ (관리자 지시 10/21): SQLite 읽기 전용 - 시험 파일은 진짜 SQLite (python sqlite3) 가 만든 것
/// (tool/fixtures/make_sqlite_fixtures.py → test/fixtures/sqlite)
void main() {
String fx(String name) => p.join('test', 'fixtures', 'sqlite', name);
test('basic.db: 테이블 · 열 이름 (INTEGER PRIMARY KEY · 따옴표 · 제약 줄 뺌) · WITHOUT ROWID 표시', () async {
final db = await SqliteFile.open(fx('basic.db'));
addTearDown(db.close);
final ts = await db.tables();
expect(ts.map((t) => t.name), ['people', 'odd name', 'kv']);
expect(ts[0].displayColumns, ['id', 'name', 'age', 'score', 'photo', 'note']);
expect(ts[1].displayColumns, ['a b', 'c', 'd', 'e'], reason: '따옴표 · [ ] · ` 이름, CONSTRAINT 줄은 빼고, DEFAULT (1+2) 의 쉼표에 속지 않음');
expect(ts[2].withoutRowid, isTrue);
await expectLater(db.rows(ts[2]), throwsA(isA<SqliteFormatException>()));
});
test('basic.db: 값 - 한글 · 일본어 · NULL · 음수 · 64비트 정수 · 실수 · BLOB · 넘친 페이지의 긴 글 · rowid = INTEGER PRIMARY KEY', () async {
final db = await SqliteFile.open(fx('basic.db'));
addTearDown(db.close);
final people = (await db.tables()).first;
final rows = await db.rows(people);
expect(rows.map((r) => r.rowid), [1, 2, 5, 7]);
expect(rows[0].values.take(4), [1, '홍길동', 30, 91.5]);
expect(rows[0].values[4], Uint8List.fromList([0, 1, 2]));
expect(rows[0].values[5], isNull);
expect(rows[1].values.sublist(2, 4), [-5, -0.25]);
expect(rows[1].values[5], 'x' * 5000, reason: '넘친 페이지 (페이지 1024 바이트) 를 이어 읽음');
expect(rows[2].values.sublist(0, 4), [5, '日本語', 9007199254740993, 1e100]);
expect(rows[2].values[4], Uint8List(0));
expect(rows[3].values.sublist(1, 3), [null, 0]);
expect(rows[3].values[5], '줄\n바꿈');
expect(await db.count(people), 4);
final odd = (await db.tables())[1];
expect((await db.rows(odd)).single.values, ['v', 1, 2, 3]);
});
test('big.db: 2000 행 (안쪽 페이지 여러 단계) - 차례대로 · 건너뛰기 · 개수', () async {
final db = await SqliteFile.open(fx('big.db'));
addTearDown(db.close);
final t = (await db.tables()).single;
expect(await db.count(t), 2000);
final first = await db.rows(t, limit: 3);
expect(first.map((r) => r.values[1]), ['row 0001 y', 'row 0002 yy', 'row 0003 yyy']);
final page = await db.rows(t, offset: 1500, limit: 2);
expect(page.map((r) => r.rowid), [1501, 1502]);
final all = await db.rows(t, limit: 5000);
expect(all.length, 2000);
expect([for (final r in all) r.rowid], [for (var i = 1; i <= 2000; i++) i], reason: 'rowid 차례');
});
test('utf16.db: UTF-16LE 글', () async {
final db = await SqliteFile.open(fx('utf16.db'));
addTearDown(db.close);
final t = (await db.tables()).single;
expect(db.encoding, 2);
expect((await db.rows(t)).map((r) => r.values.single), ['가나다', 'abc']);
});
test('SQLite 가 아닌 파일 · 잘린 파일은 SqliteFormatException (파일을 바꾸지 않음)', () async {
final tmp = Directory.systemTemp.createTempSync('jj_sqlite_');
addTearDown(() => tmp.deleteSync(recursive: true));
final txt = File(p.join(tmp.path, 'a.db'))..writeAsStringSync('hello');
await expectLater(SqliteFile.open(txt.path), throwsA(isA<SqliteFormatException>()));
// 머리만 남기고 자른 파일: 열 수는 있어도 페이지를 읽을 때 오류
final cut = File(p.join(tmp.path, 'cut.db'))..writeAsBytesSync(File(fx('big.db')).readAsBytesSync().sublist(0, 1024));
final db = await SqliteFile.open(cut.path);
addTearDown(db.close);
final t = (await db.tables()).single;
await expectLater(db.rows(t, limit: 5000), throwsA(isA<SqliteFormatException>()));
final before = File(fx('basic.db')).readAsBytesSync();
final b = await SqliteFile.open(fx('basic.db'));
await b.rows((await b.tables()).first);
await b.close();
expect(File(fx('basic.db')).readAsBytesSync(), before, reason: '읽기만');
});
Future<void> openPage(WidgetTester tester, String path, bool Function() ready) async {
tester.view.physicalSize = const Size(1200, 900);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.reset);
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
await tester.pumpWidget(MaterialApp(home: SqliteViewerPage(c: c, path: path)));
for (var i = 0; i < 500 && !ready(); i++) {
await tester.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 30)));
await tester.pump();
}
}
testWidgets('보기 화면: 첫 테이블 · 행 수 · 값 (NULL · BLOB) · 칸을 누르면 전체 값 · 다른 테이블 고르기', (tester) async {
await openPage(tester, fx('basic.db'), () => find.text('홍길동').evaluate().isNotEmpty);
expect(find.text('홍길동'), findsOneWidget);
expect(find.text('4행'), findsOneWidget);
expect(find.text('NULL'), findsWidgets);
expect(find.text('(BLOB 3바이트)'), findsOneWidget);
expect(find.textContaining('읽기 전용'), findsOneWidget);
await tester.tap(find.text('홍길동'));
await tester.pumpAndSettle();
expect(find.widgetWithText(AlertDialog, 'name'), findsOneWidget);
await tester.tap(find.text('닫기'));
await tester.pumpAndSettle();
// WITHOUT ROWID 테이블은 안내
await tester.tap(find.byType(DropdownButton<SqliteTable>));
await tester.pumpAndSettle();
await tester.tap(find.text('kv').last);
for (var i = 0; i < 20; i++) {
await tester.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 20)));
await tester.pump();
}
expect(find.text('WITHOUT ROWID 테이블은 아직 볼 수 없습니다'), findsOneWidget);
await tester.pumpWidget(const SizedBox());
});
testWidgets('보기 화면: 2000 행은 200 씩 [더 보기] · SQLite 가 아니면 알림', (tester) async {
await openPage(tester, fx('big.db'), () => find.textContaining('더 보기').evaluate().isNotEmpty);
expect(find.text('더 보기 (200 / 2000)'), findsOneWidget);
await tester.tap(find.text('더 보기 (200 / 2000)'));
for (var i = 0; i < 50 && find.text('더 보기 (400 / 2000)').evaluate().isEmpty; i++) {
await tester.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 20)));
await tester.pump();
}
expect(find.text('더 보기 (400 / 2000)'), findsOneWidget);
await tester.pumpWidget(const SizedBox());
final tmp = Directory.systemTemp.createTempSync('jj_sqlv_');
addTearDown(() => tmp.deleteSync(recursive: true));
final txt = File(p.join(tmp.path, 'x.db'))..writeAsStringSync('not sqlite');
await openPage(tester, txt.path, () => find.text('SQLite 파일이 아니거나 깨졌습니다').evaluate().isNotEmpty);
expect(find.text('SQLite 파일이 아니거나 깨졌습니다'), findsOneWidget);
await tester.pumpWidget(const SizedBox());
});
}