From 99250171fd478f545c4dca602667eb1582f3ad70 Mon Sep 17 00:00:00 2001 From: Bryan Oltman Date: Thu, 5 Oct 2023 13:47:43 -0400 Subject: [PATCH] feat(shorebird_cli): add xcodeVersion method to xcodebuild wrapper (#1360) --- .../lib/src/executables/xcodebuild.dart | 38 ++++++++ .../test/src/executables/xcodebuild_test.dart | 95 +++++++++++++++++++ 2 files changed, 133 insertions(+) diff --git a/packages/shorebird_cli/lib/src/executables/xcodebuild.dart b/packages/shorebird_cli/lib/src/executables/xcodebuild.dart index 22c006d5..765bc41d 100644 --- a/packages/shorebird_cli/lib/src/executables/xcodebuild.dart +++ b/packages/shorebird_cli/lib/src/executables/xcodebuild.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:mason_logger/mason_logger.dart'; import 'package:path/path.dart' as p; +import 'package:pub_semver/pub_semver.dart'; import 'package:scoped/scoped.dart'; import 'package:shorebird_cli/src/process.dart'; @@ -106,4 +107,41 @@ class XcodeBuild { schemes: schemes, ); } + + /// Gets the currently installed version of Xcode. + /// + /// Invokes `xcodebuild -version` and parses the output. + /// Output is expected to be of the form: + /// + /// $ /usr/bin/xcodebuild -version + /// Xcode 15.0 + /// Build version 15A240d + Future xcodeVersion() async { + const arguments = ['-version']; + final result = await process.run( + executable, + arguments, + ); + + if (result.exitCode != ExitCode.success.code) { + throw ProcessException(executable, arguments, '${result.stderr}'); + } + + final lines = LineSplitter.split('${result.stdout}').map((e) => e.trim()); + var versionString = lines.firstOrNull?.split(' ').lastOrNull; + if (versionString == null) { + throw FormatException( + 'Could not parse Xcode version from output: "${result.stdout}".', + ); + } + + // [Version.parse] requires a patch number. If Xcode does not report a patch + // number (e.g. "12.0"), add a patch number of 0 (e.g. "12.0.0") + final noPachNumberRegex = RegExp(r'^\d+\.\d+$'); + if (noPachNumberRegex.hasMatch(versionString)) { + versionString += '.0'; + } + + return Version.parse(versionString); + } } diff --git a/packages/shorebird_cli/test/src/executables/xcodebuild_test.dart b/packages/shorebird_cli/test/src/executables/xcodebuild_test.dart index b31b7667..04817ae9 100644 --- a/packages/shorebird_cli/test/src/executables/xcodebuild_test.dart +++ b/packages/shorebird_cli/test/src/executables/xcodebuild_test.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:mason_logger/mason_logger.dart'; import 'package:mocktail/mocktail.dart'; import 'package:path/path.dart' as p; +import 'package:pub_semver/pub_semver.dart'; import 'package:scoped/scoped.dart'; import 'package:shorebird_cli/src/executables/executables.dart'; import 'package:shorebird_cli/src/process.dart'; @@ -13,6 +14,7 @@ import '../mocks.dart'; void main() { group(XcodeBuild, () { late ShorebirdProcess process; + late ShorebirdProcessResult processResult; late XcodeBuild xcodeBuild; R runWithOverrides(R Function() body) { @@ -32,6 +34,7 @@ void main() { setUp(() { process = MockShorebirdProcess(); + processResult = MockShorebirdProcessResult(); xcodeBuild = runWithOverrides(XcodeBuild.new); }); @@ -132,5 +135,97 @@ To add iOS, run "flutter create . --platforms ios"''', ).called(1); }); }); + + group('xcodeVersion', () { + late ExitCode exitCode; + late String stdout; + + setUp(() { + when(() => process.run(XcodeBuild.executable, ['-version'])) + .thenAnswer((_) async => processResult); + when(() => processResult.exitCode).thenAnswer((_) => exitCode.code); + when(() => processResult.stdout).thenAnswer((_) => stdout); + }); + + group('when a non-zero exit code is returned', () { + const errorMessage = 'An unexpected error occurred.'; + setUp(() { + stdout = ''; + exitCode = ExitCode.cantCreate; + when(() => processResult.stderr).thenReturn(errorMessage); + }); + + test('throws a ProcessException', () async { + expect( + () => runWithOverrides(xcodeBuild.xcodeVersion), + throwsA( + isA() + .having((e) => e.message, 'message', errorMessage), + ), + ); + }); + }); + + group('when stdout contains unexpected output', () { + setUp(() { + exitCode = ExitCode.success; + }); + + test('throws FormatException if output is empty', () async { + stdout = ''; + expect( + () => runWithOverrides(xcodeBuild.xcodeVersion), + throwsA( + isA().having( + (e) => e.message, + 'message', + 'Could not parse Xcode version from output: "".', + ), + ), + ); + }); + + test('throws FormatException if output does not contain version', + () async { + stdout = 'unexpected output'; + expect( + () => runWithOverrides(xcodeBuild.xcodeVersion), + throwsA( + isA().having( + (e) => e.message, + 'message', + 'Could not parse "output".', + ), + ), + ); + }); + }); + + group('when stdout contains valid output', () { + setUp(() { + exitCode = ExitCode.success; + }); + + test('returns correct version with major, minor, and build numbers', + () async { + stdout = ''' +Xcode 14.3.1 +Build version 14E300c +'''; + final version = await runWithOverrides(xcodeBuild.xcodeVersion); + expect(version, Version(14, 3, 1)); + }); + + test('returns correct version with only major and minor numbers', + () async { + stdout = ''' +Xcode 15.0 +Build version 15A240d +'''; + final version = await runWithOverrides(xcodeBuild.xcodeVersion); + expect(version, Version(15, 0, 0)); + }); + }); + }); }); }