From a09cc296e30b898f205883acc45ee7e7d0fc4bbd Mon Sep 17 00:00:00 2001 From: Felix Angelov Date: Thu, 3 Aug 2023 15:16:14 -0500 Subject: [PATCH] feat(shorebird_cli): add `ShorebirdFlutterManager` (#1012) --- .../lib/src/shorebird_flutter_manager.dart | 69 ++++++++ .../src/shorebird_flutter_manager_test.dart | 164 ++++++++++++++++++ 2 files changed, 233 insertions(+) create mode 100644 packages/shorebird_cli/lib/src/shorebird_flutter_manager.dart create mode 100644 packages/shorebird_cli/test/src/shorebird_flutter_manager_test.dart diff --git a/packages/shorebird_cli/lib/src/shorebird_flutter_manager.dart b/packages/shorebird_cli/lib/src/shorebird_flutter_manager.dart new file mode 100644 index 00000000..65f89dd5 --- /dev/null +++ b/packages/shorebird_cli/lib/src/shorebird_flutter_manager.dart @@ -0,0 +1,69 @@ +import 'dart:io'; + +import 'package:path/path.dart' as p; +import 'package:shorebird_cli/src/process.dart'; +import 'package:shorebird_cli/src/shorebird_env.dart'; + +/// {@template shorebird_flutter_manager} +/// Helps manage the Flutter installation used by Shorebird. +/// {@endtemplate} +class ShorebirdFlutterManager { + /// {@macro shorebird_flutter_manager} + const ShorebirdFlutterManager(); + + static const String flutterGitUrl = + 'https://github.com/shorebirdtech/flutter.git'; + + Future installRevision({required String revision}) async { + final targetDirectory = Directory( + p.join(shorebirdEnv.flutterDirectory.parent.path, revision), + ); + if (targetDirectory.existsSync()) return; + const executable = 'git'; + + // Clone the Shorebird Flutter repo into the target directory. + final cloneArgs = [ + 'clone', + '--filter=tree:0', + flutterGitUrl, + '--no-checkout', + targetDirectory.path, + ]; + final cloneResult = await process.run( + executable, + cloneArgs, + runInShell: true, + ); + if (cloneResult.exitCode != 0) { + throw ProcessException( + executable, + cloneArgs, + '${cloneResult.stderr}', + cloneResult.exitCode, + ); + } + + // Checkout the correct revision + final checkoutArgs = [ + '-C', + targetDirectory.path, + '-c', + 'advice.detachedHead=false', + 'checkout', + revision, + ]; + final checkoutResult = await process.run( + executable, + checkoutArgs, + runInShell: true, + ); + if (checkoutResult.exitCode != 0) { + throw ProcessException( + executable, + checkoutArgs, + '${checkoutResult.stderr}', + checkoutResult.exitCode, + ); + } + } +} diff --git a/packages/shorebird_cli/test/src/shorebird_flutter_manager_test.dart b/packages/shorebird_cli/test/src/shorebird_flutter_manager_test.dart new file mode 100644 index 00000000..0224225d --- /dev/null +++ b/packages/shorebird_cli/test/src/shorebird_flutter_manager_test.dart @@ -0,0 +1,164 @@ +import 'dart:io'; + +import 'package:mason_logger/mason_logger.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:path/path.dart' as p; +import 'package:scoped/scoped.dart'; +import 'package:shorebird_cli/src/process.dart'; +import 'package:shorebird_cli/src/shorebird_env.dart'; +import 'package:shorebird_cli/src/shorebird_flutter_manager.dart'; +import 'package:test/test.dart'; + +class _MockShorebirdEnv extends Mock implements ShorebirdEnv {} + +class _MockShorebirdProcess extends Mock implements ShorebirdProcess {} + +class _MockShorebirdProcessResult extends Mock + implements ShorebirdProcessResult {} + +void main() { + group(ShorebirdFlutterManager, () { + late Directory shorebirdRoot; + late Directory flutterDirectory; + late ShorebirdEnv shorebirdEnv; + late ShorebirdProcessResult cloneProcessResult; + late ShorebirdProcessResult checkoutProcessResult; + late ShorebirdProcess shorebirdProcess; + late ShorebirdFlutterManager shorebirdFlutterManager; + + R runWithOverrides(R Function() body) { + return runScoped( + body, + values: { + processRef.overrideWith(() => shorebirdProcess), + shorebirdEnvRef.overrideWith(() => shorebirdEnv), + }, + ); + } + + setUp(() { + shorebirdRoot = Directory.systemTemp.createTempSync(); + flutterDirectory = Directory(p.join(shorebirdRoot.path, 'flutter')); + shorebirdEnv = _MockShorebirdEnv(); + cloneProcessResult = _MockShorebirdProcessResult(); + checkoutProcessResult = _MockShorebirdProcessResult(); + shorebirdProcess = _MockShorebirdProcess(); + shorebirdFlutterManager = runWithOverrides(ShorebirdFlutterManager.new); + + when(() => shorebirdEnv.flutterDirectory).thenReturn(flutterDirectory); + when( + () => shorebirdProcess.run( + any(), + any(), + runInShell: any(named: 'runInShell'), + ), + ).thenAnswer((invocation) async { + final executable = invocation.positionalArguments[0] as String; + final args = invocation.positionalArguments[1] as List; + if (executable == 'git' && args[0] == 'clone') { + return cloneProcessResult; + } else if (executable == 'git' && args[4] == 'checkout') { + return checkoutProcessResult; + } else { + throw UnimplementedError(); + } + }); + when(() => cloneProcessResult.exitCode).thenReturn(ExitCode.success.code); + when( + () => checkoutProcessResult.exitCode, + ).thenReturn(ExitCode.success.code); + }); + + group('installRevision', () { + const revision = 'test-revision'; + test('does nothing if the revision is already installed', () async { + Directory( + p.join(flutterDirectory.parent.path, revision), + ).createSync(recursive: true); + + await runWithOverrides( + () => shorebirdFlutterManager.installRevision(revision: revision), + ); + + verifyNever(() => shorebirdProcess.run(any(), any())); + }); + + test('throws ProcessException if unable to clone', () async { + when( + () => cloneProcessResult.exitCode, + ).thenReturn(ExitCode.software.code); + + await expectLater( + runWithOverrides( + () => shorebirdFlutterManager.installRevision(revision: revision), + ), + throwsA(isA()), + ); + + verify( + () => shorebirdProcess.run( + 'git', + [ + 'clone', + '--filter=tree:0', + ShorebirdFlutterManager.flutterGitUrl, + '--no-checkout', + p.join(flutterDirectory.parent.path, revision) + ], + runInShell: true, + ), + ).called(1); + }); + + test('throws ProcessException if unable to checkout revision', () async { + when( + () => checkoutProcessResult.exitCode, + ).thenReturn(ExitCode.software.code); + + await expectLater( + runWithOverrides( + () => shorebirdFlutterManager.installRevision(revision: revision), + ), + throwsA(isA()), + ); + + verify( + () => shorebirdProcess.run( + 'git', + [ + 'clone', + '--filter=tree:0', + ShorebirdFlutterManager.flutterGitUrl, + '--no-checkout', + p.join(flutterDirectory.parent.path, revision) + ], + runInShell: true, + ), + ).called(1); + verify( + () => shorebirdProcess.run( + 'git', + [ + '-C', + p.join(flutterDirectory.parent.path, revision), + '-c', + 'advice.detachedHead=false', + 'checkout', + revision, + ], + runInShell: true, + ), + ).called(1); + }); + + test('completes when clone and checkout succeed', () async { + await expectLater( + runWithOverrides( + () => shorebirdFlutterManager.installRevision(revision: revision), + ), + completes, + ); + }); + }); + }); +}