import 'dart:async'; import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; import 'package:spotiflac_android/services/ffmpeg_service.dart'; void main() { late Directory directory; setUp(() async { directory = await Directory.systemTemp.createTemp('conversion-output-'); }); tearDown(() async => directory.delete(recursive: true)); Future input(String name) => File('${directory.path}/$name').writeAsString('source'); Future succeed(List arguments) async { await File(arguments[arguments.length - 2]).writeAsString('converted'); return FFmpegResult(success: true, returnCode: 0, output: ''); } test( 'conversion preserves existing sibling and handles quoted paths', () async { final source = await input('Song "live".m4a'); final sibling = await File( '${directory.path}/Song "live".flac', ).writeAsString('existing'); final result = await FFmpegService.convertM4aToFlac( source.path, execute: (arguments) async { expect(arguments[arguments.indexOf('-i') + 1], source.path); expect(await source.exists(), isTrue); return succeed(arguments); }, ); expect(result, '${directory.path}/Song "live" (2).flac'); expect(await sibling.readAsString(), 'existing'); expect(await File(result!).readAsString(), 'converted'); expect(await source.exists(), isFalse); }, ); test('failed conversion removes partial output and retains source', () async { final source = await input('Song.m4a'); final result = await FFmpegService.convertM4aToFlac( source.path, execute: (arguments) async { await File(arguments[arguments.length - 2]).writeAsString('partial'); return FFmpegResult(success: false, returnCode: 1, output: 'failure'); }, ); expect(result, isNull); expect(await source.readAsString(), 'source'); expect(await directory.list().length, 1); }); test( 'exceptions and empty successful outputs preserve the original', () async { final source = await input('Song.m4a'); for (final execute in Function(List)>[ (_) async => throw StateError('execution failed'), (_) async => FFmpegResult(success: true, returnCode: 0, output: ''), ]) { expect( await FFmpegService.convertM4aToFlac(source.path, execute: execute), isNull, ); expect(await source.readAsString(), 'source'); expect(await directory.list().length, 1); } }, ); test('same-suffix conversion stages output until it is complete', () async { final source = await input('Song.flac'); final result = await FFmpegService.convertM4aToFlac( source.path, execute: (arguments) async { expect(arguments[arguments.length - 2], isNot(source.path)); expect(await source.readAsString(), 'source'); return succeed(arguments); }, ); expect(result, source.path); expect(await source.readAsString(), 'converted'); expect(await directory.list().length, 1); }); test('concurrent conversions reserve distinct output paths', () async { final first = await input('Song.m4a'); final second = await input('Song.mp4'); final bothStarted = Completer(); final outputPaths = {}; Future execute(List arguments) async { outputPaths.add(arguments[arguments.length - 2]); if (outputPaths.length == 2) bothStarted.complete(); await bothStarted.future.timeout(const Duration(seconds: 3)); return succeed(arguments); } final results = await Future.wait([ FFmpegService.convertM4aToFlac(first.path, execute: execute), FFmpegService.convertM4aToFlac(second.path, execute: execute), ]); expect(results.toSet(), hasLength(2)); expect(results, everyElement(isNotNull)); expect(await directory.list().length, 2); }); test( 'native FLAC rename preserves sibling files and adds missing suffix', () async { final source = await input('Song.m4a'); final sibling = await File( '${directory.path}/Song.flac', ).writeAsString('existing'); final result = await FFmpegService.ensureNativeFlacExtension(source.path); expect(result, '${directory.path}/Song (2).flac'); expect(await sibling.readAsString(), 'existing'); expect(await File(result).readAsString(), 'source'); expect(await FFmpegService.ensureNativeFlacExtension(result), result); final noSuffix = await input('Other'); expect( await FFmpegService.ensureNativeFlacExtension(noSuffix.path), '${noSuffix.path}.flac', ); }, ); test('failed native rename releases its reserved destination', () async { await expectLater( FFmpegService.ensureNativeFlacExtension('${directory.path}/missing.m4a'), throwsA(isA()), ); expect(await directory.list().length, 0); }); }