diff --git a/lib/services/file_service.dart b/lib/services/file_service.dart index 53ea0fe..c4f3ed9 100644 --- a/lib/services/file_service.dart +++ b/lib/services/file_service.dart @@ -35,18 +35,6 @@ '${dt.minute.toString().padLeft(2, '0')}' '${dt.second.toString().padLeft(2, '0')}'; - /// PNG バイト列をファイルに書き込んで保存する. - /// - /// Android ネイティブ側で YUV→RGB→PNG 変換済みのバイト列を受け取り, - /// そのまま書き込むため高速に動作する. - Future saveImage(Uint8List pngBytes) async { - await _ensureDirectory(); - final fileName = await generateFileName(); - final filePath = '$directoryPath/$fileName'; - await File(filePath).writeAsBytes(pngBytes); - return filePath; - } - /// 候補名が衝突した場合に `_1`, `_2`, ... と連番サフィックスを付与し, /// 一意な名前を決定する(同期版). /// @@ -84,19 +72,6 @@ return '${baseName}_$suffix'; } - /// ファイル名を生成する. - /// - /// 形式: MiniTIAS_YYYYMMDD_HHmmss.png - /// 同秒の重複がある場合は _1, _2, ... と連番を付与する. - Future generateFileName() async { - final baseName = 'MiniTIAS_${_buildTimestamp(DateTime.now())}'; - final resolved = await _resolveUniqueNameAsync( - baseName, - (name) => File('$directoryPath/$name.png').exists(), - ); - return '$resolved.png'; - } - /// カメラ診断情報を JSON ファイルとして保存する. /// /// ファイル名: `camera_diagnostics_YYYYMMDD_HHmmss.json` @@ -115,19 +90,6 @@ return filePath; } - /// テスト用: タイムスタンプとファイル名一覧からファイル名を生成する. - static String generateFileNameSync( - String timestamp, - List existingFiles, - ) { - final baseName = 'MiniTIAS_$timestamp'; - final resolved = _resolveUniqueName( - baseName, - (name) => existingFiles.contains('$name.png'), - ); - return '$resolved.png'; - } - /// 定量モード撮影用のファイル名 prefix を生成する. /// /// 形式: `MiniTIAS_QM_YYYYMMDD_HHmmss` diff --git a/test/services/file_service_test.dart b/test/services/file_service_test.dart index 836f0de..5daec76 100644 --- a/test/services/file_service_test.dart +++ b/test/services/file_service_test.dart @@ -7,161 +7,6 @@ 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(); @@ -322,6 +167,38 @@ }); }); + group('FileService.generateQuantitativeBaseName (async)', () { + late Directory tempDir; + late FileService service; + + setUp(() async { + tempDir = await Directory.systemTemp.createTemp( + 'smtias_capture_qm_async_test_', + ); + service = FileService(directoryPath: tempDir.path); + }); + + tearDown(() async { + if (await tempDir.exists()) { + await tempDir.delete(recursive: true); + } + }); + + test('生成した baseName の .dng が既存の場合,衝突しない名前を返す', () async { + // 実時刻に依存せず「既存 .dng と衝突しない名前を返す」契約を決定的に検証する. + // 同秒なら _1 サフィックス,秒跨ぎでも別タイムスタンプとなり,いずれにせよ + // 既存ファイルと異なる名前になる(本番の衝突回避の実挙動). + final base1 = await service.generateQuantitativeBaseName(); + await File('${tempDir.path}/$base1.dng').writeAsBytes([0]); + + final base2 = await service.generateQuantitativeBaseName(); + + expect(base2, startsWith('MiniTIAS_QM_')); + expect(base2, isNot(base1)); + expect(File('${tempDir.path}/$base2.dng').existsSync(), isFalse); + }); + }); + group('FileService.saveQuantitativeMetadata', () { late Directory tempDir; late FileService service;