feat(shorebird_cli): add shorebird upgrade (#182)
This commit is contained in:
@@ -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<ProcessResult> Function(
|
||||
String executable,
|
||||
List<String> arguments, {
|
||||
bool runInShell,
|
||||
String? workingDirectory,
|
||||
});
|
||||
|
||||
/// {@template shorebird_cli_command_runner}
|
||||
@@ -29,11 +29,9 @@ typedef RunProcess = Future<ProcessResult> Function(
|
||||
class ShorebirdCliCommandRunner extends CompletionCommandRunner<int> {
|
||||
/// {@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<int> {
|
||||
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<void> _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 (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -13,7 +13,6 @@ typedef StartProcess = Future<Process> Function(
|
||||
});
|
||||
|
||||
/// {@template run_command}
|
||||
///
|
||||
/// `shorebird run`
|
||||
/// Run the Flutter application.
|
||||
/// {@endtemplate}
|
||||
|
||||
@@ -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<int> 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;
|
||||
}
|
||||
}
|
||||
@@ -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<int> 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<String> 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<String> fetchCurrentVersion({
|
||||
required String workingDirectory,
|
||||
}) async {
|
||||
// Get the commit revision of HEAD
|
||||
return _gitRevParse('HEAD', workingDirectory: workingDirectory);
|
||||
}
|
||||
|
||||
Future<String> _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<void> 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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<String> 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 = <String>[];
|
||||
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));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = <String>[];
|
||||
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<dynamic>(() => 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'),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -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 = <String>[];
|
||||
|
||||
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);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user