From 8f52e84792a206cf22db7699a15b197afd316943 Mon Sep 17 00:00:00 2001
From: Felix Angelov <felix@shorebird.dev>
Date: Wed, 9 Aug 2023 10:30:29 -0500
Subject: [PATCH] feat(shorebird_cli): add `getVersion` to `ShorebirdFlutter`
 (#1073)

---
 .../flutter_versions_use_command.dart         |  8 +-
 .../lib/src/shorebird_flutter.dart            | 26 ++++++
 .../lib/src/shorebird_version.dart            |  2 +-
 .../flutter_versions_use_command_test.dart    |  8 +-
 .../test/src/shorebird_flutter_test.dart      | 89 +++++++++++++++----
 5 files changed, 114 insertions(+), 19 deletions(-)

diff --git a/packages/shorebird_cli/lib/src/commands/flutter/versions/flutter_versions_use_command.dart b/packages/shorebird_cli/lib/src/commands/flutter/versions/flutter_versions_use_command.dart
index dd143a469..e23599e04 100644
--- a/packages/shorebird_cli/lib/src/commands/flutter/versions/flutter_versions_use_command.dart
+++ b/packages/shorebird_cli/lib/src/commands/flutter/versions/flutter_versions_use_command.dart
@@ -51,8 +51,14 @@ Usage: shorebird flutter versions use <version>''');
     }
 
     if (!versions.contains(version)) {
+      final openIssueLink = link(
+        uri: Uri.parse(
+          'https://github.com/shorebirdtech/shorebird/issues/new?assignees=&labels=feature&projects=&template=feature_request.md&title=feat%3A+',
+        ),
+        message: 'open an issue',
+      );
       logger.err('''
-Version $version not found.
+Version $version not found. Please $openIssueLink to request a new version.
 Use `shorebird flutter versions list` to list available versions.''');
       return ExitCode.software.code;
     }
diff --git a/packages/shorebird_cli/lib/src/shorebird_flutter.dart b/packages/shorebird_cli/lib/src/shorebird_flutter.dart
index f69042cab..b65c2fa66 100644
--- a/packages/shorebird_cli/lib/src/shorebird_flutter.dart
+++ b/packages/shorebird_cli/lib/src/shorebird_flutter.dart
@@ -4,6 +4,7 @@ import 'dart:io';
 import 'package:path/path.dart' as p;
 import 'package:scoped/scoped.dart';
 import 'package:shorebird_cli/src/git.dart';
+import 'package:shorebird_cli/src/process.dart';
 import 'package:shorebird_cli/src/shorebird_env.dart';
 
 /// A reference to a [ShorebirdFlutter] instance.
@@ -19,6 +20,7 @@ class ShorebirdFlutter {
   /// {@macro shorebird_flutter}
   const ShorebirdFlutter();
 
+  static const executable = 'flutter';
   static const String flutterGitUrl =
       'https://github.com/shorebirdtech/flutter.git';
 
@@ -62,6 +64,30 @@ class ShorebirdFlutter {
     return status.isEmpty;
   }
 
+  /// Returns the current Shorebird Flutter version.
+  /// Throws a [ProcessException] if the version check fails.
+  /// Returns `null` if the version check succeeds but the version cannot be
+  /// parsed.
+  Future<String?> getVersion() async {
+    const args = ['--version'];
+    final result = await process.run(executable, args, runInShell: true);
+
+    if (result.exitCode != 0) {
+      throw ProcessException(
+        executable,
+        args,
+        '${result.stderr}',
+        result.exitCode,
+      );
+    }
+
+    final output = result.stdout.toString();
+    final flutterVersionRegex = RegExp(r'Flutter (\d+.\d+.\d+)');
+    final match = flutterVersionRegex.firstMatch(output);
+
+    return match?.group(1);
+  }
+
   Future<List<String>> getVersions({String? revision}) async {
     final result = await git.forEachRef(
       format: '%(refname:short)',
diff --git a/packages/shorebird_cli/lib/src/shorebird_version.dart b/packages/shorebird_cli/lib/src/shorebird_version.dart
index fea9e054e..b08cb15da 100644
--- a/packages/shorebird_cli/lib/src/shorebird_version.dart
+++ b/packages/shorebird_cli/lib/src/shorebird_version.dart
@@ -10,7 +10,7 @@ final shorebirdVersionRef = create(ShorebirdVersion.new);
 /// The [ShorebirdVersion] instance available in the current zone.
 ShorebirdVersion get shorebirdVersion => read(shorebirdVersionRef);
 
-/// {@template shorebird_version_manager}
+/// {@template shorebird_version}
 /// Provides information about installed and available versions of Shorebird.
 /// {@endtemplate}
 class ShorebirdVersion {
diff --git a/packages/shorebird_cli/test/src/commands/flutter/versions/flutter_versions_use_command_test.dart b/packages/shorebird_cli/test/src/commands/flutter/versions/flutter_versions_use_command_test.dart
index 3583f7b75..1b29f3b24 100644
--- a/packages/shorebird_cli/test/src/commands/flutter/versions/flutter_versions_use_command_test.dart
+++ b/packages/shorebird_cli/test/src/commands/flutter/versions/flutter_versions_use_command_test.dart
@@ -106,11 +106,17 @@ Usage: shorebird flutter versions use <version>'''),
         runWithOverrides(command.run),
         completion(equals(ExitCode.software.code)),
       );
+      final openIssueLink = link(
+        uri: Uri.parse(
+          'https://github.com/shorebirdtech/shorebird/issues/new?assignees=&labels=feature&projects=&template=feature_request.md&title=feat%3A+',
+        ),
+        message: 'open an issue',
+      );
       verifyInOrder([
         () => logger.progress('Fetching Flutter versions'),
         () => progress.complete(),
         () => logger.err('''
-Version $version not found.
+Version $version not found. Please $openIssueLink to request a new version.
 Use `shorebird flutter versions list` to list available versions.'''),
       ]);
     });
diff --git a/packages/shorebird_cli/test/src/shorebird_flutter_test.dart b/packages/shorebird_cli/test/src/shorebird_flutter_test.dart
index 703bdbf1a..6c5c53b9f 100644
--- a/packages/shorebird_cli/test/src/shorebird_flutter_test.dart
+++ b/packages/shorebird_cli/test/src/shorebird_flutter_test.dart
@@ -5,6 +5,7 @@ import 'package:mocktail/mocktail.dart';
 import 'package:path/path.dart' as p;
 import 'package:scoped/scoped.dart';
 import 'package:shorebird_cli/src/git.dart';
+import 'package:shorebird_cli/src/process.dart';
 import 'package:shorebird_cli/src/shorebird_env.dart';
 import 'package:shorebird_cli/src/shorebird_flutter.dart';
 import 'package:test/test.dart';
@@ -13,6 +14,11 @@ class _MockGit extends Mock implements Git {}
 
 class _MockShorebirdEnv extends Mock implements ShorebirdEnv {}
 
+class _MockShorebirdProcess extends Mock implements ShorebirdProcess {}
+
+class _MockShorebirdProcessResult extends Mock
+    implements ShorebirdProcessResult {}
+
 void main() {
   group(ShorebirdFlutter, () {
     const flutterRevision = 'flutter-revision';
@@ -20,13 +26,16 @@ void main() {
     late Directory flutterDirectory;
     late Git git;
     late ShorebirdEnv shorebirdEnv;
-    late ShorebirdFlutter shorebirdFlutterManager;
+    late ShorebirdProcess process;
+    late ShorebirdProcessResult processResult;
+    late ShorebirdFlutter shorebirdFlutter;
 
     R runWithOverrides<R>(R Function() body) {
       return runScoped(
         body,
         values: {
           gitRef.overrideWith(() => git),
+          processRef.overrideWith(() => process),
           shorebirdEnvRef.overrideWith(() => shorebirdEnv),
         },
       );
@@ -37,7 +46,9 @@ void main() {
       flutterDirectory = Directory(p.join(shorebirdRoot.path, 'flutter'));
       git = _MockGit();
       shorebirdEnv = _MockShorebirdEnv();
-      shorebirdFlutterManager = runWithOverrides(ShorebirdFlutter.new);
+      process = _MockShorebirdProcess();
+      processResult = _MockShorebirdProcessResult();
+      shorebirdFlutter = runWithOverrides(ShorebirdFlutter.new);
 
       when(
         () => git.clone(
@@ -72,6 +83,52 @@ void main() {
       ).thenAnswer((_) async => flutterRevision);
       when(() => shorebirdEnv.flutterDirectory).thenReturn(flutterDirectory);
       when(() => shorebirdEnv.flutterRevision).thenReturn(flutterRevision);
+      when(
+        () => process.run('flutter', ['--version'], runInShell: true),
+      ).thenAnswer((_) async => processResult);
+      when(() => processResult.exitCode).thenReturn(0);
+    });
+
+    group('getVersion', () {
+      test('throws ProcessException when process exits with non-zero code',
+          () async {
+        const error = 'oops';
+        when(() => processResult.exitCode).thenReturn(ExitCode.software.code);
+        when(() => processResult.stderr).thenReturn(error);
+        await expectLater(
+          runWithOverrides(shorebirdFlutter.getVersion),
+          throwsA(isA<ProcessException>()),
+        );
+        verify(
+          () => process.run('flutter', ['--version'], runInShell: true),
+        ).called(1);
+      });
+
+      test('returns null when cannot parse version', () async {
+        when(() => processResult.stdout).thenReturn('');
+        await expectLater(
+          runWithOverrides(shorebirdFlutter.getVersion),
+          completion(isNull),
+        );
+        verify(
+          () => process.run('flutter', ['--version'], runInShell: true),
+        ).called(1);
+      });
+
+      test('returns version when able to parse the string', () async {
+        when(() => processResult.stdout).thenReturn('''
+Flutter 3.10.6 • channel stable • git@github.com:flutter/flutter.git
+Framework • revision f468f3366c (4 weeks ago) • 2023-07-12 15:19:05 -0700
+Engine • revision cdbeda788a
+Tools • Dart 3.0.6 • DevTools 2.23.1''');
+        await expectLater(
+          runWithOverrides(shorebirdFlutter.getVersion),
+          completion(equals('3.10.6')),
+        );
+        verify(
+          () => process.run('flutter', ['--version'], runInShell: true),
+        ).called(1);
+      });
     });
 
     group('getVersions', () {
@@ -104,7 +161,7 @@ origin/flutter_release/3.10.6''';
         ).thenAnswer((_) async => output);
 
         await expectLater(
-          runWithOverrides(shorebirdFlutterManager.getVersions),
+          runWithOverrides(shorebirdFlutter.getVersions),
           completion(equals(versions)),
         );
         verify(
@@ -135,7 +192,7 @@ origin/flutter_release/3.10.6''';
         );
 
         expect(
-          runWithOverrides(shorebirdFlutterManager.getVersions),
+          runWithOverrides(shorebirdFlutter.getVersions),
           throwsA(
             isA<ProcessException>().having(
               (e) => e.message,
@@ -155,7 +212,7 @@ origin/flutter_release/3.10.6''';
         ).createSync(recursive: true);
 
         await runWithOverrides(
-          () => shorebirdFlutterManager.installRevision(revision: revision),
+          () => shorebirdFlutter.installRevision(revision: revision),
         );
 
         verifyNever(
@@ -179,7 +236,7 @@ origin/flutter_release/3.10.6''';
 
         await expectLater(
           runWithOverrides(
-            () => shorebirdFlutterManager.installRevision(revision: revision),
+            () => shorebirdFlutter.installRevision(revision: revision),
           ),
           throwsA(exception),
         );
@@ -204,7 +261,7 @@ origin/flutter_release/3.10.6''';
 
         await expectLater(
           runWithOverrides(
-            () => shorebirdFlutterManager.installRevision(revision: revision),
+            () => shorebirdFlutter.installRevision(revision: revision),
           ),
           throwsA(exception),
         );
@@ -226,7 +283,7 @@ origin/flutter_release/3.10.6''';
       test('completes when clone and checkout succeed', () async {
         await expectLater(
           runWithOverrides(
-            () => shorebirdFlutterManager.installRevision(revision: revision),
+            () => shorebirdFlutter.installRevision(revision: revision),
           ),
           completes,
         );
@@ -236,7 +293,7 @@ origin/flutter_release/3.10.6''';
     group('pruneRemoteOrigin', () {
       test('completes when git command exits with code 0', () async {
         await expectLater(
-          runWithOverrides(() => shorebirdFlutterManager.pruneRemoteOrigin()),
+          runWithOverrides(() => shorebirdFlutter.pruneRemoteOrigin()),
           completes,
         );
         verify(
@@ -252,7 +309,7 @@ origin/flutter_release/3.10.6''';
         const customRevision = 'custom-revision';
         await expectLater(
           runWithOverrides(
-            () => shorebirdFlutterManager.pruneRemoteOrigin(
+            () => shorebirdFlutter.pruneRemoteOrigin(
               revision: customRevision,
             ),
           ),
@@ -284,7 +341,7 @@ origin/flutter_release/3.10.6''';
         );
 
         expect(
-          runWithOverrides(() => shorebirdFlutterManager.pruneRemoteOrigin()),
+          runWithOverrides(() => shorebirdFlutter.pruneRemoteOrigin()),
           throwsA(
             isA<ProcessException>().having(
               (e) => e.message,
@@ -299,7 +356,7 @@ origin/flutter_release/3.10.6''';
     group('isPorcelain', () {
       test('returns true when status is empty', () async {
         await expectLater(
-          runWithOverrides(() => shorebirdFlutterManager.isPorcelain()),
+          runWithOverrides(() => shorebirdFlutter.isPorcelain()),
           completion(isTrue),
         );
         verify(
@@ -318,7 +375,7 @@ origin/flutter_release/3.10.6''';
           ),
         ).thenAnswer((_) async => 'M some/file');
         await expectLater(
-          runWithOverrides(() => shorebirdFlutterManager.isPorcelain()),
+          runWithOverrides(() => shorebirdFlutter.isPorcelain()),
           completion(isFalse),
         );
         verify(
@@ -347,7 +404,7 @@ origin/flutter_release/3.10.6''';
         );
 
         expect(
-          runWithOverrides(() => shorebirdFlutterManager.isPorcelain()),
+          runWithOverrides(() => shorebirdFlutter.isPorcelain()),
           throwsA(
             isA<ProcessException>().having(
               (e) => e.message,
@@ -375,7 +432,7 @@ origin/flutter_release/3.10.6''';
       test('installs revision if it does not exist', () async {
         await expectLater(
           runWithOverrides(
-            () => shorebirdFlutterManager.useVersion(version: version),
+            () => shorebirdFlutter.useVersion(version: version),
           ),
           completes,
         );
@@ -403,7 +460,7 @@ origin/flutter_release/3.10.6''';
             .createSync(recursive: true);
         await expectLater(
           runWithOverrides(
-            () => shorebirdFlutterManager.useVersion(version: version),
+            () => shorebirdFlutter.useVersion(version: version),
           ),
           completes,
         );