diff --git a/packages/shorebird_cli/lib/src/command_runner.dart b/packages/shorebird_cli/lib/src/command_runner.dart index 092746c1..4be60caf 100644 --- a/packages/shorebird_cli/lib/src/command_runner.dart +++ b/packages/shorebird_cli/lib/src/command_runner.dart @@ -4,7 +4,6 @@ import 'package:args/args.dart'; import 'package:args/command_runner.dart'; import 'package:cli_completion/cli_completion.dart'; import 'package:mason_logger/mason_logger.dart'; -import 'package:pub_updater/pub_updater.dart'; import 'package:shorebird_cli/src/commands/commands.dart'; import 'package:shorebird_cli/src/flutter_engine_revision.dart'; import 'package:shorebird_cli/src/version.dart'; @@ -17,6 +16,7 @@ typedef RunProcess = Future Function( String executable, List arguments, { bool runInShell, + String? workingDirectory, }); /// {@template shorebird_cli_command_runner} @@ -29,11 +29,9 @@ typedef RunProcess = Future Function( class ShorebirdCliCommandRunner extends CompletionCommandRunner { /// {@macro shorebird_cli_command_runner} ShorebirdCliCommandRunner({ - PubUpdater? pubUpdater, Logger? logger, RunProcess? runProcess, }) : _logger = logger ?? Logger(), - _pubUpdater = pubUpdater ?? PubUpdater(), _runProcess = runProcess ?? Process.run, super(executableName, description) { argParser @@ -55,14 +53,13 @@ class ShorebirdCliCommandRunner extends CompletionCommandRunner { addCommand(LogoutCommand(logger: _logger)); addCommand(PublishCommand(logger: _logger)); addCommand(RunCommand(logger: _logger)); - addCommand(UpdateCommand(logger: _logger, pubUpdater: pubUpdater)); + addCommand(UpgradeCommand(logger: _logger)); } @override void printUsage() => _logger.info(usage); final Logger _logger; - final PubUpdater _pubUpdater; final RunProcess _runProcess; @override @@ -134,11 +131,6 @@ Detected engine revision: "$flutterEngineRevision"''', exitCode = await super.runCommand(topLevelResults); } - // Check for updates - if (topLevelResults.command?.name != UpdateCommand.commandName) { - await _checkForUpdates(); - } - return exitCode; } @@ -158,23 +150,4 @@ Detected engine revision: "$flutterEngineRevision"''', } return flutterEngineRevision; } - - /// Checks if the current version (set by the build runner on the - /// version.dart file) is the most recent one. If not, show a prompt to the - /// user. - Future _checkForUpdates() async { - try { - final latestVersion = await _pubUpdater.getLatestVersion(packageName); - final isUpToDate = packageVersion == latestVersion; - if (!isUpToDate) { - _logger - ..info('') - ..info( - ''' -${lightYellow.wrap('Update available!')} ${lightCyan.wrap(packageVersion)} \u2192 ${lightCyan.wrap(latestVersion)} -Run ${lightCyan.wrap('$executableName update')} to update''', - ); - } - } catch (_) {} - } } diff --git a/packages/shorebird_cli/lib/src/commands/commands.dart b/packages/shorebird_cli/lib/src/commands/commands.dart index 8a2da5fd..4652211b 100644 --- a/packages/shorebird_cli/lib/src/commands/commands.dart +++ b/packages/shorebird_cli/lib/src/commands/commands.dart @@ -5,4 +5,4 @@ export 'login_command.dart'; export 'logout_command.dart'; export 'publish_command.dart'; export 'run_command.dart'; -export 'update_command.dart'; +export 'upgrade_command.dart'; diff --git a/packages/shorebird_cli/lib/src/commands/run_command.dart b/packages/shorebird_cli/lib/src/commands/run_command.dart index 6d1354b1..a30f397f 100644 --- a/packages/shorebird_cli/lib/src/commands/run_command.dart +++ b/packages/shorebird_cli/lib/src/commands/run_command.dart @@ -13,7 +13,6 @@ typedef StartProcess = Future Function( }); /// {@template run_command} -/// /// `shorebird run` /// Run the Flutter application. /// {@endtemplate} diff --git a/packages/shorebird_cli/lib/src/commands/update_command.dart b/packages/shorebird_cli/lib/src/commands/update_command.dart deleted file mode 100644 index c2cbdfde..00000000 --- a/packages/shorebird_cli/lib/src/commands/update_command.dart +++ /dev/null @@ -1,72 +0,0 @@ -import 'dart:io'; - -import 'package:mason_logger/mason_logger.dart'; -import 'package:pub_updater/pub_updater.dart'; -import 'package:shorebird_cli/src/command.dart'; -import 'package:shorebird_cli/src/command_runner.dart'; -import 'package:shorebird_cli/src/version.dart'; - -/// {@template update_command} -/// A command which updates the CLI. -/// {@endtemplate} -class UpdateCommand extends ShorebirdCommand { - /// {@macro update_command} - UpdateCommand({ - required super.logger, - PubUpdater? pubUpdater, - }) : _pubUpdater = pubUpdater ?? PubUpdater(); - - final PubUpdater _pubUpdater; - - @override - String get description => 'Update the CLI.'; - - static const String commandName = 'update'; - - @override - String get name => commandName; - - @override - Future run() async { - final updateCheckProgress = logger.progress('Checking for updates'); - late final String latestVersion; - try { - latestVersion = await _pubUpdater.getLatestVersion(packageName); - } catch (error) { - updateCheckProgress.fail(); - logger.err('$error'); - return ExitCode.software.code; - } - updateCheckProgress.complete('Checked for updates'); - - final isUpToDate = packageVersion == latestVersion; - if (isUpToDate) { - logger.info('CLI is already at the latest version.'); - return ExitCode.success.code; - } - - final updateProgress = logger.progress('Updating to $latestVersion'); - - late final ProcessResult result; - try { - result = await _pubUpdater.update( - packageName: packageName, - versionConstraint: latestVersion, - ); - } catch (error) { - updateProgress.fail(); - logger.err('$error'); - return ExitCode.software.code; - } - - if (result.exitCode != ExitCode.success.code) { - updateProgress.fail(); - logger.err('Error updating CLI: ${result.stderr}'); - return ExitCode.software.code; - } - - updateProgress.complete('Updated to $latestVersion'); - - return ExitCode.success.code; - } -} diff --git a/packages/shorebird_cli/lib/src/commands/upgrade_command.dart b/packages/shorebird_cli/lib/src/commands/upgrade_command.dart new file mode 100644 index 00000000..333f0324 --- /dev/null +++ b/packages/shorebird_cli/lib/src/commands/upgrade_command.dart @@ -0,0 +1,144 @@ +import 'dart:io'; + +import 'package:mason_logger/mason_logger.dart'; +import 'package:path/path.dart' as p; +import 'package:shorebird_cli/src/command.dart'; + +/// {@template upgrade_command} +/// `shorebird upgrade` +/// A command which upgrades your copy of Shorebird. +/// {@endtemplate} +class UpgradeCommand extends ShorebirdCommand { + /// {@macro upgrade_command} + UpgradeCommand({required super.logger, super.runProcess}); + + @override + String get description => 'Upgrade your copy of Shorebird.'; + + static const String commandName = 'upgrade'; + + @override + String get name => commandName; + + @override + Future run() async { + final updateCheckProgress = logger.progress('Checking for updates'); + final workingDirectory = p.dirname(Platform.script.toFilePath()); + + late final String currentVersion; + try { + currentVersion = await fetchCurrentVersion( + workingDirectory: workingDirectory, + ); + } on ProcessException catch (error) { + updateCheckProgress.fail(); + logger.err('Fetching current version failed: ${error.message}'); + return ExitCode.software.code; + } + + late final String latestVersion; + try { + latestVersion = await fetchLatestVersion( + workingDirectory: workingDirectory, + ); + } on ProcessException catch (error) { + updateCheckProgress.fail(); + logger.err('Checking for updates failed: ${error.message}'); + return ExitCode.software.code; + } + + updateCheckProgress.complete('Checked for updates'); + + final isUpToDate = currentVersion == latestVersion; + if (isUpToDate) { + logger.info('Shorebird is already at the latest version.'); + return ExitCode.success.code; + } + + final updateProgress = logger.progress('Updating'); + + try { + await attemptReset( + newRevision: latestVersion, + workingDirectory: workingDirectory, + ); + } on ProcessException catch (error) { + updateProgress.fail(); + logger.err('Updating failed: ${error.message}'); + return ExitCode.software.code; + } + + updateProgress.complete('Updated successfully.'); + + return ExitCode.success.code; + } + + /// Returns the remote HEAD shorebird version. + /// + /// Exits if HEAD isn't pointing to a branch, or there is no upstream. + Future fetchLatestVersion({required String workingDirectory}) async { + // Fetch upstream branch's commits and tags + await runProcess( + 'git', + ['fetch', '--tags'], + workingDirectory: workingDirectory, + ); + // Get the latest commit revision of the upstream + return _gitRevParse('@{upstream}', workingDirectory: workingDirectory); + } + + /// Returns the local HEAD shorebird version. + /// + /// Exits if HEAD isn't pointing to a branch, or there is no upstream. + Future fetchCurrentVersion({ + required String workingDirectory, + }) async { + // Get the commit revision of HEAD + return _gitRevParse('HEAD', workingDirectory: workingDirectory); + } + + Future _gitRevParse( + String revision, { + String? workingDirectory, + }) async { +// Get the commit revision of HEAD + final result = await runProcess( + 'git', + ['rev-parse', '--verify', revision], + workingDirectory: workingDirectory, + ); + if (result.exitCode != 0) { + throw ProcessException( + 'git', + ['rev-parse', '--verify', revision], + '${result.stderr}', + result.exitCode, + ); + } + return '${result.stdout}'.trim(); + } + + /// Attempts a hard reset to the given revision. + /// + /// This is a reset instead of fast forward because if we are on a release + /// branch with cherry picks, there may not be a direct fast-forward route + /// to the next release. + Future attemptReset({ + required String newRevision, + required String workingDirectory, + }) async { + final result = await runProcess( + 'git', + ['reset', '--hard', newRevision], + workingDirectory: workingDirectory, + ); + if (result.exitCode != 0) { + throw ProcessException( + 'git', + ['reset', '--hard', newRevision], + '${result.stderr}', + result.exitCode, + ); + } + } +} diff --git a/packages/shorebird_cli/pubspec.yaml b/packages/shorebird_cli/pubspec.yaml index babd719c..8df1d296 100644 --- a/packages/shorebird_cli/pubspec.yaml +++ b/packages/shorebird_cli/pubspec.yaml @@ -20,7 +20,6 @@ dependencies: mason_logger: ^0.2.4 meta: ^1.9.0 path: ^1.8.3 - pub_updater: ^0.2.4 pubspec_parse: ^1.2.2 shorebird_code_push_client: path: ../shorebird_code_push_client diff --git a/packages/shorebird_cli/test/src/command_runner_test.dart b/packages/shorebird_cli/test/src/command_runner_test.dart index ffb7f00f..8ad22344 100644 --- a/packages/shorebird_cli/test/src/command_runner_test.dart +++ b/packages/shorebird_cli/test/src/command_runner_test.dart @@ -4,7 +4,6 @@ import 'package:args/command_runner.dart'; import 'package:cli_completion/cli_completion.dart'; import 'package:mason_logger/mason_logger.dart'; import 'package:mocktail/mocktail.dart'; -import 'package:pub_updater/pub_updater.dart'; import 'package:shorebird_cli/src/command_runner.dart'; import 'package:shorebird_cli/src/flutter_engine_revision.dart'; import 'package:shorebird_cli/src/version.dart'; @@ -14,30 +13,13 @@ class _MockLogger extends Mock implements Logger {} class _MockProcessResult extends Mock implements ProcessResult {} -class _MockProgress extends Mock implements Progress {} - -class _MockPubUpdater extends Mock implements PubUpdater {} - -const latestVersion = '0.0.0'; - -final updatePrompt = ''' -${lightYellow.wrap('Update available!')} ${lightCyan.wrap(packageVersion)} \u2192 ${lightCyan.wrap(latestVersion)} -Run ${lightCyan.wrap('$executableName update')} to update'''; - void main() { group('ShorebirdCliCommandRunner', () { - late PubUpdater pubUpdater; late Logger logger; late ProcessResult processResult; late ShorebirdCliCommandRunner commandRunner; setUp(() { - pubUpdater = _MockPubUpdater(); - - when( - () => pubUpdater.getLatestVersion(any()), - ).thenAnswer((_) async => packageVersion); - logger = _MockLogger(); processResult = _MockProcessResult(); @@ -48,11 +30,11 @@ void main() { commandRunner = ShorebirdCliCommandRunner( logger: logger, - pubUpdater: pubUpdater, runProcess: ( - String executable, - List arguments, { + executable, + arguments, { bool runInShell = false, + String? workingDirectory, }) async { return processResult; }, @@ -113,60 +95,6 @@ Tools • Dart 2.19.4 • DevTools 2.20.1 ).called(1); }); - test('shows update message when newer version exists', () async { - when( - () => pubUpdater.getLatestVersion(any()), - ).thenAnswer((_) async => latestVersion); - - final result = await commandRunner.run(['--version']); - expect(result, equals(ExitCode.success.code)); - verify(() => logger.info(updatePrompt)).called(1); - }); - - test( - 'Does not show update message when the shell calls the ' - 'completion command', - () async { - when( - () => pubUpdater.getLatestVersion(any()), - ).thenAnswer((_) async => latestVersion); - - final result = await commandRunner.run(['completion']); - expect(result, equals(ExitCode.success.code)); - verifyNever(() => logger.info(updatePrompt)); - }, - ); - - test('does not show update message when using update command', () async { - when( - () => pubUpdater.getLatestVersion(any()), - ).thenAnswer((_) async => latestVersion); - when( - () => pubUpdater.update( - packageName: packageName, - versionConstraint: any(named: 'versionConstraint'), - ), - ).thenAnswer((_) async => processResult); - when( - () => pubUpdater.isUpToDate( - packageName: any(named: 'packageName'), - currentVersion: any(named: 'currentVersion'), - ), - ).thenAnswer((_) async => true); - - final progress = _MockProgress(); - final progressLogs = []; - when(() => progress.complete(any())).thenAnswer((_) { - final message = _.positionalArguments.elementAt(0) as String?; - if (message != null) progressLogs.add(message); - }); - when(() => logger.progress(any())).thenReturn(progress); - - final result = await commandRunner.run(['update']); - expect(result, equals(ExitCode.success.code)); - verifyNever(() => logger.info(updatePrompt)); - }); - test('can be instantiated without an explicit analytics/logger instance', () { final commandRunner = ShorebirdCliCommandRunner(); @@ -218,5 +146,12 @@ Tools • Dart 2.19.4 • DevTools 2.20.1 expect(result, equals(ExitCode.success.code)); }); }); + + group('completion', () { + test('fast tracks completion', () async { + final result = await commandRunner.run(['completion']); + expect(result, equals(ExitCode.success.code)); + }); + }); }); } diff --git a/packages/shorebird_cli/test/src/commands/build_command_test.dart b/packages/shorebird_cli/test/src/commands/build_command_test.dart index 6030ca97..170877a1 100644 --- a/packages/shorebird_cli/test/src/commands/build_command_test.dart +++ b/packages/shorebird_cli/test/src/commands/build_command_test.dart @@ -49,7 +49,12 @@ void main() { return codePushClient; }, logger: logger, - runProcess: (executable, arguments, {bool runInShell = false}) async { + runProcess: ( + executable, + arguments, { + bool runInShell = false, + String? workingDirectory, + }) async { return processResult; }, )..testArgResults = argResults; diff --git a/packages/shorebird_cli/test/src/commands/publish_command_test.dart b/packages/shorebird_cli/test/src/commands/publish_command_test.dart index c3647d28..0b4a872a 100644 --- a/packages/shorebird_cli/test/src/commands/publish_command_test.dart +++ b/packages/shorebird_cli/test/src/commands/publish_command_test.dart @@ -95,7 +95,12 @@ flutter: capturedHostedUri = hostedUri; return codePushClient; }, - runProcess: (executable, arguments, {bool runInShell = false}) async { + runProcess: ( + executable, + arguments, { + bool runInShell = false, + String? workingDirectory, + }) async { return processResult; }, logger: logger, diff --git a/packages/shorebird_cli/test/src/commands/update_command_test.dart b/packages/shorebird_cli/test/src/commands/update_command_test.dart deleted file mode 100644 index 234bbe5e..00000000 --- a/packages/shorebird_cli/test/src/commands/update_command_test.dart +++ /dev/null @@ -1,188 +0,0 @@ -import 'dart:io'; - -import 'package:mason_logger/mason_logger.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:pub_updater/pub_updater.dart'; -import 'package:shorebird_cli/src/command_runner.dart'; -import 'package:shorebird_cli/src/commands/commands.dart'; -import 'package:shorebird_cli/src/version.dart'; -import 'package:test/test.dart'; - -class _MockLogger extends Mock implements Logger {} - -class _MockProcessResult extends Mock implements ProcessResult {} - -class _MockProgress extends Mock implements Progress {} - -class _MockPubUpdater extends Mock implements PubUpdater {} - -void main() { - const latestVersion = '0.0.0'; - - group('update', () { - late PubUpdater pubUpdater; - late Logger logger; - late ProcessResult processResult; - late UpdateCommand command; - - setUp(() { - final progress = _MockProgress(); - final progressLogs = []; - pubUpdater = _MockPubUpdater(); - logger = _MockLogger(); - processResult = _MockProcessResult(); - command = UpdateCommand( - logger: logger, - pubUpdater: pubUpdater, - ); - - when( - () => pubUpdater.getLatestVersion(any()), - ).thenAnswer((_) async => packageVersion); - when( - () => pubUpdater.update( - packageName: packageName, - versionConstraint: latestVersion, - ), - ).thenAnswer((_) async => processResult); - when( - () => pubUpdater.isUpToDate( - packageName: any(named: 'packageName'), - currentVersion: any(named: 'currentVersion'), - ), - ).thenAnswer((_) async => true); - when(() => progress.complete(any())).thenAnswer((_) { - final message = _.positionalArguments.elementAt(0) as String?; - if (message != null) progressLogs.add(message); - }); - when(() => logger.progress(any())).thenReturn(progress); - when(() => processResult.exitCode).thenReturn(ExitCode.success.code); - }); - - test('can be instantiated without a pub updater', () { - final command = UpdateCommand(logger: logger); - expect(command, isNotNull); - }); - - test( - 'handles pub latest version query errors', - () async { - when( - () => pubUpdater.getLatestVersion(any()), - ).thenThrow(Exception('oops')); - final result = await command.run(); - expect(result, equals(ExitCode.software.code)); - verify(() => logger.progress('Checking for updates')).called(1); - verify(() => logger.err('Exception: oops')); - verifyNever( - () => pubUpdater.update( - packageName: any(named: 'packageName'), - versionConstraint: any(named: 'versionConstraint'), - ), - ); - }, - ); - - test( - 'handles pub update errors', - () async { - when( - () => pubUpdater.getLatestVersion(any()), - ).thenAnswer((_) async => latestVersion); - when( - () => pubUpdater.update( - packageName: any(named: 'packageName'), - versionConstraint: any(named: 'versionConstraint'), - ), - ).thenThrow(Exception('oops')); - final result = await command.run(); - expect(result, equals(ExitCode.software.code)); - verify(() => logger.progress('Checking for updates')).called(1); - verify(() => logger.err('Exception: oops')); - verify( - () => pubUpdater.update( - packageName: any(named: 'packageName'), - versionConstraint: any(named: 'versionConstraint'), - ), - ).called(1); - }, - ); - - test('handles pub update process errors', () async { - const error = 'Oh no! Installing this is not possible right now!'; - - when(() => processResult.exitCode).thenReturn(1); - when(() => processResult.stderr).thenReturn(error); - when( - () => pubUpdater.getLatestVersion(any()), - ).thenAnswer((_) async => latestVersion); - - when( - () => pubUpdater.update( - packageName: any(named: 'packageName'), - versionConstraint: any(named: 'versionConstraint'), - ), - ).thenAnswer((_) async => processResult); - - final result = await command.run(); - - expect(result, equals(ExitCode.software.code)); - verify(() => logger.progress('Checking for updates')).called(1); - verify(() => logger.err('Error updating CLI: $error')); - verify( - () => pubUpdater.update( - packageName: any(named: 'packageName'), - versionConstraint: any(named: 'versionConstraint'), - ), - ).called(1); - }); - - test( - 'updates when newer version exists', - () async { - when( - () => pubUpdater.getLatestVersion(any()), - ).thenAnswer((_) async => latestVersion); - when( - () => pubUpdater.update( - packageName: any(named: 'packageName'), - versionConstraint: any(named: 'versionConstraint'), - ), - ).thenAnswer((_) async => processResult); - when(() => logger.progress(any())).thenReturn(_MockProgress()); - final result = await command.run(); - expect(result, equals(ExitCode.success.code)); - verify(() => logger.progress('Checking for updates')).called(1); - verify(() => logger.progress('Updating to $latestVersion')).called(1); - verify( - () => pubUpdater.update( - packageName: packageName, - versionConstraint: latestVersion, - ), - ).called(1); - }, - ); - - test( - 'does not update when already on latest version', - () async { - when( - () => pubUpdater.getLatestVersion(any()), - ).thenAnswer((_) async => packageVersion); - when(() => logger.progress(any())).thenReturn(_MockProgress()); - final result = await command.run(); - expect(result, equals(ExitCode.success.code)); - verify( - () => logger.info('CLI is already at the latest version.'), - ).called(1); - verifyNever(() => logger.progress('Updating to $latestVersion')); - verifyNever( - () => pubUpdater.update( - packageName: any(named: 'packageName'), - versionConstraint: any(named: 'versionConstraint'), - ), - ); - }, - ); - }); -} diff --git a/packages/shorebird_cli/test/src/commands/upgrade_test.dart b/packages/shorebird_cli/test/src/commands/upgrade_test.dart new file mode 100644 index 00000000..ed3335a7 --- /dev/null +++ b/packages/shorebird_cli/test/src/commands/upgrade_test.dart @@ -0,0 +1,163 @@ +import 'dart:io'; + +import 'package:mason_logger/mason_logger.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:shorebird_cli/src/commands/commands.dart'; +import 'package:test/test.dart'; + +class _MockLogger extends Mock implements Logger {} + +class _MockProcessResult extends Mock implements ProcessResult {} + +class _MockProgress extends Mock implements Progress {} + +void main() { + const currentShorebirdRevision = 'revision-1'; + const newerShorebirdRevision = 'revision-2'; + + group('upgrade', () { + late Logger logger; + late ProcessResult fetchCurrentVersionResult; + late ProcessResult fetchTagsResult; + late ProcessResult fetchLatestVersionResult; + late ProcessResult hardResetResult; + late UpgradeCommand command; + + setUp(() { + final progress = _MockProgress(); + final progressLogs = []; + + logger = _MockLogger(); + fetchCurrentVersionResult = _MockProcessResult(); + fetchTagsResult = _MockProcessResult(); + fetchLatestVersionResult = _MockProcessResult(); + hardResetResult = _MockProcessResult(); + command = UpgradeCommand( + logger: logger, + runProcess: ( + executable, + arguments, { + bool runInShell = false, + workingDirectory, + }) async { + if (executable == 'git') { + const revParseHead = ['rev-parse', '--verify', 'HEAD']; + if (arguments.every((arg) => revParseHead.contains(arg))) { + return fetchCurrentVersionResult; + } + + const fetchTags = ['fetch', '--tags']; + if (arguments.every((arg) => fetchTags.contains(arg))) { + return fetchTagsResult; + } + + const revParseUpstream = ['rev-parse', '--verify', '@{upstream}']; + if (arguments.every((arg) => revParseUpstream.contains(arg))) { + return fetchLatestVersionResult; + } + + const resetHard = ['reset', '--hard', newerShorebirdRevision]; + if (arguments.every((arg) => resetHard.contains(arg))) { + return hardResetResult; + } + } + return _MockProcessResult(); + }, + ); + + when( + () => fetchCurrentVersionResult.exitCode, + ).thenReturn(ExitCode.success.code); + when( + () => fetchCurrentVersionResult.stdout, + ).thenReturn(currentShorebirdRevision); + when( + () => fetchLatestVersionResult.exitCode, + ).thenReturn(ExitCode.success.code); + when( + () => fetchLatestVersionResult.stdout, + ).thenReturn(currentShorebirdRevision); + when(() => hardResetResult.exitCode).thenReturn(ExitCode.success.code); + when(() => progress.complete(any())).thenAnswer((_) { + final message = _.positionalArguments.elementAt(0) as String?; + if (message != null) progressLogs.add(message); + }); + when(() => logger.progress(any())).thenReturn(progress); + }); + + test('can be instantiated', () { + final command = UpgradeCommand(logger: logger); + expect(command, isNotNull); + }); + + test( + 'handles errors when determining the current version', + () async { + const errorMessage = 'oops'; + when(() => fetchCurrentVersionResult.exitCode).thenReturn(1); + when(() => fetchCurrentVersionResult.stderr).thenReturn(errorMessage); + final result = await command.run(); + expect(result, equals(ExitCode.software.code)); + verify(() => logger.progress('Checking for updates')).called(1); + verify( + () => logger.err('Fetching current version failed: $errorMessage'), + ).called(1); + }, + ); + + test( + 'handles errors when determining the latest version', + () async { + const errorMessage = 'oops'; + when(() => fetchLatestVersionResult.exitCode).thenReturn(1); + when(() => fetchLatestVersionResult.stderr).thenReturn(errorMessage); + final result = await command.run(); + expect(result, equals(ExitCode.software.code)); + verify(() => logger.progress('Checking for updates')).called(1); + verify(() => logger.err('Checking for updates failed: oops')).called(1); + }, + ); + + test( + 'handles errors when updating', + () async { + const errorMessage = 'oops'; + when( + () => fetchLatestVersionResult.stdout, + ).thenReturn(newerShorebirdRevision); + when(() => hardResetResult.exitCode).thenReturn(1); + when(() => hardResetResult.stderr).thenReturn(errorMessage); + final result = await command.run(); + expect(result, equals(ExitCode.software.code)); + verify(() => logger.progress('Checking for updates')).called(1); + verify(() => logger.err('Updating failed: oops')).called(1); + }, + ); + + test( + 'updates when newer version exists', + () async { + when( + () => fetchLatestVersionResult.stdout, + ).thenReturn(newerShorebirdRevision); + when(() => logger.progress(any())).thenReturn(_MockProgress()); + final result = await command.run(); + expect(result, equals(ExitCode.success.code)); + verify(() => logger.progress('Checking for updates')).called(1); + verify(() => logger.progress('Updating')).called(1); + }, + ); + + test( + 'does not update when already on latest version', + () async { + when(() => logger.progress(any())).thenReturn(_MockProgress()); + final result = await command.run(); + expect(result, equals(ExitCode.success.code)); + verify( + () => logger.info('Shorebird is already at the latest version.'), + ).called(1); + }, + ); + }); +}