Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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