mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-13 21:38:58 +02:00
Reuse the conversion output lifecycle for M4A conversion and native FLAC suffix correction. Reserve destination names atomically, reject empty output, and clean partial files while preserving the input on failure. Add regressions for existing outputs, concurrent conversions, staging, and cleanup.
140 lines
5.0 KiB
Dart
140 lines
5.0 KiB
Dart
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<File> input(String name) =>
|
|
File('${directory.path}/$name').writeAsString('source');
|
|
|
|
Future<FFmpegResult> succeed(List<String> 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 <Future<FFmpegResult> Function(List<String>)>[
|
|
(_) 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<void>();
|
|
final outputPaths = <String>{};
|
|
Future<FFmpegResult> execute(List<String> 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<FileSystemException>()),
|
|
);
|
|
expect(await directory.list().length, 0);
|
|
});
|
|
}
|