import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; import 'package:flutter_test/flutter_test.dart'; import 'package:smtias_capture/services/file_service.dart'; void main() { group('FileService.generateFileNameSync', () { test('同秒のファイルが存在しない場合,サフィックスなしのファイル名を返す', () { final result = FileService.generateFileNameSync('20260404_120000', []); expect(result, 'MiniTIAS_20260404_120000.png'); }); test('同秒のファイルが存在する場合,_1 サフィックスを付与する', () { final result = FileService.generateFileNameSync('20260404_120000', [ 'MiniTIAS_20260404_120000.png', ]); expect(result, 'MiniTIAS_20260404_120000_1.png'); }); test('_1 も存在する場合,_2 サフィックスを付与する', () { final result = FileService.generateFileNameSync('20260404_120000', [ 'MiniTIAS_20260404_120000.png', 'MiniTIAS_20260404_120000_1.png', ]); expect(result, 'MiniTIAS_20260404_120000_2.png'); }); test('異なるタイムスタンプのファイルが存在しても影響しない', () { final result = FileService.generateFileNameSync('20260404_120000', [ 'MiniTIAS_20260404_120001.png', ]); expect(result, 'MiniTIAS_20260404_120000.png'); }); // --- 追加テスト: ファイル命名規則の詳細検証 --- test('返り値が MiniTIAS_ プレフィックスで始まる', () { final result = FileService.generateFileNameSync('20260404_120000', []); expect(result, startsWith('MiniTIAS_')); }); test('返り値が .png 拡張子で終わる', () { final result = FileService.generateFileNameSync('20260404_120000', []); expect(result, endsWith('.png')); }); test('連番が連続して欠けている場合(_1 が欠落),_2 ではなく _1 を返す', () { // 仕様: suffix=1 から順に探すため _1 が存在しなければ _1 を返す final result = FileService.generateFileNameSync('20260404_120000', [ 'MiniTIAS_20260404_120000.png', 'MiniTIAS_20260404_120000_2.png', // _1 が存在しない状態 ]); expect(result, 'MiniTIAS_20260404_120000_1.png'); }); test('連番が多数存在する場合,次の番号を付与する', () { final existing = [ 'MiniTIAS_20260404_120000.png', 'MiniTIAS_20260404_120000_1.png', 'MiniTIAS_20260404_120000_2.png', 'MiniTIAS_20260404_120000_3.png', 'MiniTIAS_20260404_120000_4.png', ]; final result = FileService.generateFileNameSync( '20260404_120000', existing, ); expect(result, 'MiniTIAS_20260404_120000_5.png'); }); test('空のタイムスタンプでも形式を維持する', () { final result = FileService.generateFileNameSync('', []); expect(result, 'MiniTIAS_.png'); }); }); group('FileService.saveImage', () { late Directory tempDir; late FileService service; setUp(() async { tempDir = await Directory.systemTemp.createTemp('smtias_capture_test_'); service = FileService(directoryPath: tempDir.path); }); tearDown(() async { if (await tempDir.exists()) { await tempDir.delete(recursive: true); } }); test('PNG バイト列を渡すと,ファイルパスを返す', () async { final pngBytes = Uint8List.fromList([0, 1, 2, 3]); final path = await service.saveImage(pngBytes); expect(path, isNotEmpty); }); test('PNG バイト列を渡すと,.png 拡張子のファイルが生成される', () async { final pngBytes = Uint8List.fromList([0, 1, 2, 3]); final path = await service.saveImage(pngBytes); expect(path, endsWith('.png')); }); test('PNG バイト列を渡すと,ファイル名が MiniTIAS_ プレフィックスを持つ', () async { final pngBytes = Uint8List.fromList([0, 1, 2, 3]); final path = await service.saveImage(pngBytes); final fileName = path.split('/').last; expect(fileName, startsWith('MiniTIAS_')); }); test('渡したバイト列がそのままファイルに書き込まれる', () async { final pngBytes = Uint8List.fromList([137, 80, 78, 71, 13, 10, 26, 10]); final path = await service.saveImage(pngBytes); final written = await File(path).readAsBytes(); expect(written, equals(pngBytes)); }); test('空のバイト列でもファイルを生成できる', () async { final pngBytes = Uint8List(0); final path = await service.saveImage(pngBytes); expect(await File(path).exists(), isTrue); final written = await File(path).readAsBytes(); expect(written.length, 0); }); test('同秒に 2 回呼び出すと,2 つ目のファイル名に _1 サフィックスが付く', () async { final pngBytes = Uint8List.fromList([0, 1, 2, 3]); // 最初の saveImage でファイルを作成 final path1 = await service.saveImage(pngBytes); // 同秒内に再度呼び出す(ファイルが既に存在する状態を擬似的に作る) // generateFileName はファイルシステムを参照するため,1 ファイル作成後に呼ぶ final path2 = await service.saveImage(pngBytes); final fileName1 = path1.split('/').last; final fileName2 = path2.split('/').last; // 2 つのファイル名は異なるはず // (同秒の場合 _1 サフィックスが付く,異なる秒の場合も名前が異なる) if (fileName1.replaceAll(RegExp(r'_\d+\.png$'), '.png') == fileName2.replaceAll(RegExp(r'_\d+\.png$'), '.png')) { // 同秒の場合: 2 つ目は _1 サフィックスを持つはず expect(fileName2, contains('_1.png')); } else { // 異なる秒の場合: それぞれ独立したタイムスタンプ付きファイル名 expect(fileName1, isNot(equals(fileName2))); } }); test('ディレクトリが存在しない場合でも自動作成してファイルを保存する', () async { final nonExistentSubDir = Directory('${tempDir.path}/sub/dir'); final serviceWithSubDir = FileService( directoryPath: nonExistentSubDir.path, ); expect(await nonExistentSubDir.exists(), isFalse); final pngBytes = Uint8List.fromList([0, 1, 2, 3]); final path = await serviceWithSubDir.saveImage(pngBytes); expect(await File(path).exists(), isTrue); }); }); group('FileService.directoryPath', () { test('保存先ディレクトリのパスを返す', () { final service = FileService(); expect(service.directoryPath, '/storage/emulated/0/Pictures/MiniTIAS'); }); }); group('FileService.savePreviewJpeg', () { late Directory tempDir; late FileService service; setUp(() async { tempDir = await Directory.systemTemp.createTemp( 'smtias_capture_preview_test_', ); service = FileService(directoryPath: tempDir.path); }); tearDown(() async { if (await tempDir.exists()) { await tempDir.delete(recursive: true); } }); test('指定した baseName で .preview.jpg ファイルが生成される', () async { const baseName = 'MiniTIAS_QM_20260531_120000'; final previewBytes = Uint8List.fromList([0xFF, 0xD8, 0xFF, 0xE0]); await service.savePreviewJpeg(baseName, previewBytes); final expectedFile = File('${tempDir.path}/$baseName.preview.jpg'); expect(await expectedFile.exists(), isTrue); }); test('戻り値が保存パス(baseName.preview.jpg を含む)になる', () async { const baseName = 'MiniTIAS_QM_20260531_120000'; final previewBytes = Uint8List.fromList([0xFF, 0xD8, 0xFF, 0xE0]); final path = await service.savePreviewJpeg(baseName, previewBytes); expect(path, contains('$baseName.preview.jpg')); }); test('渡したバイト列がそのままファイルに書き込まれる', () async { const baseName = 'MiniTIAS_QM_20260531_120001'; final previewBytes = Uint8List.fromList([ 0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, ]); final path = await service.savePreviewJpeg(baseName, previewBytes); final written = await File(path).readAsBytes(); expect(written, equals(previewBytes)); }); test('ディレクトリが存在しない場合でも自動作成してファイルを保存する', () async { final nonExistentSubDir = Directory('${tempDir.path}/sub/dir'); final serviceWithSubDir = FileService( directoryPath: nonExistentSubDir.path, ); expect(await nonExistentSubDir.exists(), isFalse); const baseName = 'MiniTIAS_QM_20260531_120002'; final previewBytes = Uint8List.fromList([0xFF, 0xD8, 0xFF, 0xE0]); final path = await serviceWithSubDir.savePreviewJpeg( baseName, previewBytes, ); expect(await File(path).exists(), isTrue); }); test('空のバイト列でも .preview.jpg ファイルが生成される', () async { const baseName = 'MiniTIAS_QM_20260531_120003'; final previewBytes = Uint8List(0); final path = await service.savePreviewJpeg(baseName, previewBytes); expect(await File(path).exists(), isTrue); final written = await File(path).readAsBytes(); expect(written.length, 0); }); }); group('FileService.generateQuantitativeBaseNameSync', () { const timestamp = '20260404_120000'; const baseName = 'MiniTIAS_QM_$timestamp'; // 「タイムスタンプ+既存ファイル一覧 → baseName」の等価検証は, // 入力(existing)と期待値だけが異なる同型ケースのためテーブル駆動でまとめる. // 重複判定は `.dng` の存在チェックのみで行い,suffix=1 から順に空きを探す仕様. final cases = <({String name, List existing, String expected})>[ ( name: '同秒のファイルが存在しない場合,サフィックスなしの baseName を返す', existing: [], expected: baseName, ), ( name: '同秒の .dng が存在する場合,_1 サフィックスを付与する', existing: ['$baseName.dng'], expected: '${baseName}_1', ), ( name: '_1.dng も存在する場合,_2 サフィックスを付与する', existing: ['$baseName.dng', '${baseName}_1.dng'], expected: '${baseName}_2', ), ( name: '異なるタイムスタンプの .dng が存在しても影響しない', existing: ['MiniTIAS_QM_20260404_120001.dng'], expected: baseName, ), ( // _1 が存在しないため,欠番の _1 を返す name: '連番に欠番がある場合(_1.dng が欠落),_2 ではなく _1 を返す', existing: ['$baseName.dng', '${baseName}_2.dng'], expected: '${baseName}_1', ), ( name: '連番が多数存在する場合(_1〜_5.dng),_6 を付与する', existing: [ '$baseName.dng', '${baseName}_1.dng', '${baseName}_2.dng', '${baseName}_3.dng', '${baseName}_4.dng', '${baseName}_5.dng', ], expected: '${baseName}_6', ), ( // 重複判定は .dng のみを見るため,.meta.json のみでは重複とみなさない name: '候補 .meta.json のみが存在し .dng がない場合,サフィックスなしを返す', existing: ['$baseName.meta.json'], expected: baseName, ), ]; for (final c in cases) { test(c.name, () { final result = FileService.generateQuantitativeBaseNameSync( timestamp, c.existing, ); expect(result, c.expected); }); } test('返り値が MiniTIAS_QM_ プレフィックスで始まる', () { final result = FileService.generateQuantitativeBaseNameSync( timestamp, [], ); expect(result, startsWith('MiniTIAS_QM_')); }); }); group('FileService.saveQuantitativeMetadata', () { late Directory tempDir; late FileService service; setUp(() async { tempDir = await Directory.systemTemp.createTemp( 'smtias_capture_qm_test_', ); service = FileService(directoryPath: tempDir.path); }); tearDown(() async { if (await tempDir.exists()) { await tempDir.delete(recursive: true); } }); test('LSC マップなしで呼び出すと .meta.json 拡張子のファイルが生成される', () async { final metadata = {'settings': {}}; final path = await service.saveQuantitativeMetadata( 'MiniTIAS_QM_20260404_120000', metadata, ); expect(path, endsWith('.meta.json')); expect(await File(path).exists(), isTrue); }); test( 'LSC マップなしの JSON には lscMap / lscMapRowCount / lscMapColumnCount キーが含まれない', () async { final metadata = {'settings': {}}; final path = await service.saveQuantitativeMetadata( 'MiniTIAS_QM_20260404_120000', metadata, ); final jsonStr = await File(path).readAsString(); final decoded = jsonDecode(jsonStr) as Map; expect(decoded.containsKey('lscMap'), isFalse); expect(decoded.containsKey('lscMapRowCount'), isFalse); expect(decoded.containsKey('lscMapColumnCount'), isFalse); }, ); test('LSC マップありで呼び出すと JSON 内に lscMap の二重配列が含まれる', () async { final metadata = {'settings': {}}; final lscMap = [ [1.0, 2.0], [3.0, 4.0], ]; final path = await service.saveQuantitativeMetadata( 'MiniTIAS_QM_20260404_120000', metadata, lscMap: lscMap, lscMapRowCount: 2, lscMapColumnCount: 2, ); final jsonStr = await File(path).readAsString(); final decoded = jsonDecode(jsonStr) as Map; expect(decoded.containsKey('lscMap'), isTrue); final storedLscMap = decoded['lscMap'] as List; expect(storedLscMap, hasLength(2)); expect(storedLscMap[0], isA>()); }); test( 'LSC マップありの JSON で lscMapRowCount と lscMapColumnCount が指定値で保存される', () async { final metadata = {'settings': {}}; final lscMap = [ [1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0], ]; final path = await service.saveQuantitativeMetadata( 'MiniTIAS_QM_20260404_120000', metadata, lscMap: lscMap, lscMapRowCount: 3, lscMapColumnCount: 3, ); final jsonStr = await File(path).readAsString(); final decoded = jsonDecode(jsonStr) as Map; expect(decoded['lscMapRowCount'], 3); expect(decoded['lscMapColumnCount'], 3); }, ); test('メタデータの中身が JSON 内に保持される', () async { final metadata = { 'settings': {'exposure_time_ns': 10000000, 'iso': 400}, 'actual': {'frame_duration_ns': 33333333}, 'sensorCharacteristics': {'make': 'TestMake'}, }; final path = await service.saveQuantitativeMetadata( 'MiniTIAS_QM_20260404_120000', metadata, ); final jsonStr = await File(path).readAsString(); final decoded = jsonDecode(jsonStr) as Map; expect(decoded.containsKey('settings'), isTrue); expect(decoded.containsKey('actual'), isTrue); expect(decoded.containsKey('sensorCharacteristics'), isTrue); final settings = decoded['settings'] as Map; expect(settings['iso'], 400); }); test('ディレクトリが存在しない場合でも自動作成してファイルを保存する', () async { final nonExistentSubDir = Directory('${tempDir.path}/sub/dir'); final serviceWithSubDir = FileService( directoryPath: nonExistentSubDir.path, ); expect(await nonExistentSubDir.exists(), isFalse); final metadata = {'settings': {}}; final path = await serviceWithSubDir.saveQuantitativeMetadata( 'MiniTIAS_QM_20260404_120000', metadata, ); expect(await File(path).exists(), isTrue); }); }); group('FileService.saveDiagnosticsJson', () { late Directory tempDir; late FileService service; setUp(() async { tempDir = await Directory.systemTemp.createTemp( 'smtias_capture_diag_test_', ); service = FileService(directoryPath: tempDir.path); }); tearDown(() async { if (await tempDir.exists()) { await tempDir.delete(recursive: true); } }); test('camera_diagnostics_ プレフィックスの .json ファイルが生成される', () async { final diagnostics = {'make': 'TestMake'}; final path = await service.saveDiagnosticsJson(diagnostics); final fileName = path.split('/').last; expect(fileName, startsWith('camera_diagnostics_')); expect(fileName, endsWith('.json')); expect(await File(path).exists(), isTrue); }); test('診断内容が JSON として保持される', () async { final diagnostics = { 'make': 'TestMake', 'model': 'TestModel', 'sensor': { 'iso_range': [40, 800], }, }; final path = await service.saveDiagnosticsJson(diagnostics); final jsonStr = await File(path).readAsString(); final decoded = jsonDecode(jsonStr) as Map; expect(decoded['make'], 'TestMake'); expect(decoded['model'], 'TestModel'); expect((decoded['sensor'] as Map)['iso_range'], [ 40, 800, ]); }); test('インデント付き(pretty print)で書き込まれる', () async { final diagnostics = {'make': 'TestMake'}; final path = await service.saveDiagnosticsJson(diagnostics); final jsonStr = await File(path).readAsString(); // JsonEncoder.withIndent(' ') により改行とインデントを含む expect(jsonStr, contains('\n')); expect(jsonStr, contains(' "make"')); }); test('ディレクトリが存在しない場合でも自動作成して保存する', () async { final nonExistentSubDir = Directory('${tempDir.path}/sub/dir'); final serviceWithSubDir = FileService( directoryPath: nonExistentSubDir.path, ); expect(await nonExistentSubDir.exists(), isFalse); final path = await serviceWithSubDir.saveDiagnosticsJson({'make': 'X'}); expect(await File(path).exists(), isTrue); }); }); }