diff --git a/packages/shorebird_cli/lib/src/platform/apple/plist.dart b/packages/shorebird_cli/lib/src/platform/apple/plist.dart index 50484419..a35147cf 100644 --- a/packages/shorebird_cli/lib/src/platform/apple/plist.dart +++ b/packages/shorebird_cli/lib/src/platform/apple/plist.dart @@ -1,18 +1,39 @@ -// cspell:words propertylistserialization xcarchives +// cspell:words propertylistserialization xcarchives plutil import 'dart:io'; import 'package:propertylistserialization/propertylistserialization.dart'; +/// Exception thrown when a plist file cannot be parsed. +class PlistParseException implements Exception { + /// Creates a new [PlistParseException]. + const PlistParseException({required this.filePath, required this.cause}); + + /// The path to the plist file that failed to parse. + final String filePath; + + /// The underlying exception that caused the parse failure. + final Exception cause; + + @override + String toString() => + 'Failed to parse $filePath: $cause\n' + 'Verify the plist is valid by running: plutil -lint $filePath'; +} + /// A representation of an Info.plist file. class Plist { /// Creates a new [Plist] from the contents of the provided [file]. Plist({required File file}) { - properties = - PropertyListSerialization.propertyListWithString( - file.readAsStringSync(), - ) - as Map; + try { + properties = + PropertyListSerialization.propertyListWithString( + file.readAsStringSync(), + ) + as Map; + } on PropertyListReadStreamException catch (e) { + throw PlistParseException(filePath: file.path, cause: e); + } } /// This key is a user-visible string for the version of the bundle. The diff --git a/packages/shorebird_cli/test/src/platform/apple/plist_test.dart b/packages/shorebird_cli/test/src/platform/apple/plist_test.dart new file mode 100644 index 00000000..da5b78e3 --- /dev/null +++ b/packages/shorebird_cli/test/src/platform/apple/plist_test.dart @@ -0,0 +1,108 @@ +// cspell:words plutil +import 'dart:io'; + +import 'package:path/path.dart' as p; +import 'package:shorebird_cli/src/platform/apple/plist.dart'; +import 'package:test/test.dart'; + +void main() { + group('Plist', () { + late Directory tempDir; + + setUp(() { + tempDir = Directory.systemTemp.createTempSync('plist_test_'); + }); + + tearDown(() { + tempDir.deleteSync(recursive: true); + }); + + group('constructor', () { + test('throws PlistParseException when plist is malformed', () { + final file = File(p.join(tempDir.path, 'Info.plist')) + ..writeAsStringSync('not valid plist xml'); + + expect( + () => Plist(file: file), + throwsA( + isA() + .having( + (e) => e.filePath, + 'filePath', + file.path, + ) + .having( + (e) => e.toString(), + 'toString', + allOf( + contains('Failed to parse'), + contains('plutil -lint'), + ), + ), + ), + ); + }); + }); + + group('versionNumber', () { + test('returns version from standard plist', () { + final file = File(p.join(tempDir.path, 'Info.plist')) + ..writeAsStringSync(''' + + + + + CFBundleShortVersionString + 1.2.3 + CFBundleVersion + 4 + + +'''); + + expect(Plist(file: file).versionNumber, equals('1.2.3+4')); + }); + + test('returns version without build number when not present', () { + final file = File(p.join(tempDir.path, 'Info.plist')) + ..writeAsStringSync(''' + + + + + CFBundleShortVersionString + 1.0.0 + + +'''); + + expect(Plist(file: file).versionNumber, equals('1.0.0')); + }); + + test('throws when release version is missing', () { + final file = File(p.join(tempDir.path, 'Info.plist')) + ..writeAsStringSync(''' + + + + + CFBundleVersion + 1 + + +'''); + + expect( + () => Plist(file: file).versionNumber, + throwsA( + isA().having( + (e) => e.toString(), + 'message', + contains('Could not determine release version'), + ), + ), + ); + }); + }); + }); +}