Skip to content

Commit

Permalink
feat: add auto update (#181)
Browse files Browse the repository at this point in the history
  • Loading branch information
denangelov committed Sep 22, 2021
1 parent 193eb30 commit b8b490b
Show file tree
Hide file tree
Showing 4 changed files with 135 additions and 2 deletions.
42 changes: 40 additions & 2 deletions lib/src/command_runner.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'package:args/command_runner.dart';
import 'package:io/ansi.dart';
import 'package:io/io.dart';
import 'package:mason/mason.dart';
import 'package:pub_updater/pub_updater.dart';
import 'package:usage/usage_io.dart';
import 'package:very_good_cli/src/commands/commands.dart';
import 'package:very_good_cli/src/version.dart';
Expand All @@ -13,15 +14,22 @@ const _gaTrackingId = 'UA-117465969-4';
// The Google Analytics Application Name.
const _gaAppName = 'very-good-cli';

/// The package name.
const packageName = 'very_good_cli';

/// {@template very_good_command_runner}
/// A [CommandRunner] for the Very Good CLI.
/// {@endtemplate}
class VeryGoodCommandRunner extends CommandRunner<int> {
/// {@macro very_good_command_runner}
VeryGoodCommandRunner({Analytics? analytics, Logger? logger})
: _logger = logger ?? Logger(),
VeryGoodCommandRunner({
Analytics? analytics,
Logger? logger,
PubUpdater? pubUpdater,
}) : _logger = logger ?? Logger(),
_analytics =
analytics ?? AnalyticsIO(_gaTrackingId, _gaAppName, packageVersion),
_pubUpdater = pubUpdater ?? PubUpdater(),
super('very_good', '馃 A Very Good Command Line Interface') {
argParser
..addFlag(
Expand All @@ -46,6 +54,7 @@ class VeryGoodCommandRunner extends CommandRunner<int> {

final Logger _logger;
final Analytics _analytics;
final PubUpdater _pubUpdater;

@override
Future<int> run(Iterable<String> args) async {
Expand Down Expand Up @@ -85,6 +94,7 @@ class VeryGoodCommandRunner extends CommandRunner<int> {

@override
Future<int?> runCommand(ArgResults topLevelResults) async {
await _checkForUpdates();
if (topLevelResults['version'] == true) {
_logger.info('very_good version: $packageVersion');
return ExitCode.success.code;
Expand All @@ -97,4 +107,32 @@ class VeryGoodCommandRunner extends CommandRunner<int> {
}
return super.runCommand(topLevelResults);
}

Future<void> _checkForUpdates() async {
try {
final isUpToDate = await _pubUpdater.isUpToDate(
packageName: packageName,
currentVersion: packageVersion,
);

if (!isUpToDate) {
_logger.info(
lightYellow.wrap('A new release of $packageName is available.'),
);
final response = _logger.prompt('Would you like to update? (y/n) ');
if (response.isYes()) {
final done = _logger.progress('Updating');
await _pubUpdater.update(packageName: packageName);
done('Updated!');
}
}
} catch (_) {}
}
}

extension on String {
bool isYes() {
final normalized = toLowerCase().trim();
return normalized == 'y' || normalized == 'yes';
}
}
1 change: 1 addition & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ dependencies:
mason: ^0.0.1-dev.46
meta: ^1.3.0
path: ^1.8.0
pub_updater: ^0.1.0
universal_io: ^2.0.4
usage: ^4.0.2
very_good_analysis: ^2.3.0
Expand Down
79 changes: 79 additions & 0 deletions test/src/command_runner_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@
import 'dart:async';

import 'package:args/command_runner.dart';
import 'package:io/ansi.dart';
import 'package:io/io.dart';
import 'package:mason/mason.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pub_updater/pub_updater.dart';
import 'package:test/test.dart';
import 'package:universal_io/io.dart';
import 'package:usage/usage_io.dart';
import 'package:very_good_cli/src/command_runner.dart';
import 'package:very_good_cli/src/version.dart';
Expand All @@ -14,6 +17,10 @@ class MockAnalytics extends Mock implements Analytics {}

class MockLogger extends Mock implements Logger {}

class MockPubUpdater extends Mock implements PubUpdater {}

class FakeProcessResult extends Fake implements ProcessResult {}

const expectedUsage = [
'馃 A Very Good Command Line Interface\n'
'\n'
Expand All @@ -34,10 +41,14 @@ const expectedUsage = [
'Run "very_good help <command>" for more information about a command.'
];

const responseBody =
'{"name": "very_good_cli", "versions": ["0.4.0", "0.3.3"]}';

void main() {
group('VeryGoodCommandRunner', () {
late List<String> printLogs;
late Analytics analytics;
late PubUpdater pubUpdater;
late Logger logger;
late VeryGoodCommandRunner commandRunner;

Expand All @@ -54,13 +65,30 @@ void main() {
printLogs = [];

analytics = MockAnalytics();
pubUpdater = MockPubUpdater();

when(() => analytics.firstRun).thenReturn(false);
when(() => analytics.enabled).thenReturn(false);

when(
() => pubUpdater.isUpToDate(
packageName: any(named: 'packageName'),
currentVersion: any(named: 'currentVersion'),
),
).thenAnswer((_) => Future.value(true));

when(
() => pubUpdater.update(
packageName: any(named: 'packageName'),
),
).thenAnswer((_) => Future.value(FakeProcessResult()));

logger = MockLogger();

commandRunner = VeryGoodCommandRunner(
analytics: analytics,
logger: logger,
pubUpdater: pubUpdater,
);
});

Expand All @@ -71,6 +99,57 @@ void main() {
});

group('run', () {
test('prompts for update when newer version exists', () async {
when(() => pubUpdater.isUpToDate(
packageName: any(named: 'packageName'),
currentVersion: any(named: 'currentVersion'),
)).thenAnswer((_) => Future.value(false));

when(() => logger.prompt(any())).thenReturn('n');

final result = await commandRunner.run(['--version']);
expect(result, equals(ExitCode.success.code));
verify(
() => logger.info(
lightYellow.wrap('A new release of $packageName is available.'),
),
).called(1);
verify(
() => logger.prompt('Would you like to update? (y/n) '),
).called(1);
});

test('handles pub update errors gracefully', () async {
when(
() => pubUpdater.isUpToDate(
packageName: any(named: 'packageName'),
currentVersion: any(named: 'currentVersion'),
),
).thenThrow(Exception('oops'));

final result = await commandRunner.run(['--version']);
expect(result, equals(ExitCode.success.code));
verifyNever(
() => logger.info(
lightYellow.wrap('A new release of $packageName is available.'),
),
);
});

test('updates on "y" response when newer version exists', () async {
when(() => pubUpdater.isUpToDate(
packageName: any(named: 'packageName'),
currentVersion: any(named: 'currentVersion'),
)).thenAnswer((_) => Future.value(false));

when(() => logger.prompt(any())).thenReturn('y');
when(() => logger.progress(any())).thenReturn(([String? message]) {});

final result = await commandRunner.run(['--version']);
expect(result, equals(ExitCode.success.code));
verify(() => logger.progress('Updating')).called(1);
});

test('prompts for analytics collection on first run (y)', () async {
when(() => analytics.firstRun).thenReturn(true);
when(() => logger.prompt(any())).thenReturn('y');
Expand Down
15 changes: 15 additions & 0 deletions test/src/commands/create_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'package:io/io.dart';
import 'package:mason/mason.dart';
import 'package:mocktail/mocktail.dart';
import 'package:path/path.dart' as p;
import 'package:pub_updater/pub_updater.dart';
import 'package:test/test.dart';
import 'package:universal_io/io.dart';
import 'package:usage/usage_io.dart';
Expand Down Expand Up @@ -45,6 +46,8 @@ class MockAnalytics extends Mock implements Analytics {}

class MockLogger extends Mock implements Logger {}

class MockPubUpdater extends Mock implements PubUpdater {}

class MockMasonGenerator extends Mock implements MasonGenerator {}

class FakeDirectoryGeneratorTarget extends Fake
Expand All @@ -56,6 +59,7 @@ void main() {
late List<String> printLogs;
late Analytics analytics;
late Logger logger;
late PubUpdater pubUpdater;
late VeryGoodCommandRunner commandRunner;

void Function() overridePrint(void Function() fn) {
Expand Down Expand Up @@ -90,9 +94,20 @@ void main() {
if (_ != null) progressLogs.add(_);
},
);

pubUpdater = MockPubUpdater();

when(
() => pubUpdater.isUpToDate(
packageName: any(named: 'packageName'),
currentVersion: any(named: 'currentVersion'),
),
).thenAnswer((_) => Future.value(true));

commandRunner = VeryGoodCommandRunner(
analytics: analytics,
logger: logger,
pubUpdater: pubUpdater,
);
});

Expand Down

0 comments on commit b8b490b

Please sign in to comment.