From 13685b3c5ddf8405c5ffcda85ac0b8dd798dc2fd Mon Sep 17 00:00:00 2001 From: Bryan Oltman Date: Thu, 15 Jun 2023 16:03:03 -0400 Subject: [PATCH] refactor(shorebird_cli): migrate ReleaseAarCommand to use CodePushClientWrapper (#665) --- .../lib/src/code_push_client_wrapper.dart | 65 +++ .../commands/release/release_aar_command.dart | 163 ++------ .../lib/src/shorebird_artifact_mixin.dart | 11 +- .../src/code_push_client_wrapper_test.dart | 275 ++++++++++++- .../patch/patch_aar_command_test.dart | 1 + .../release/release_aar_command_test.dart | 369 +++++------------- 6 files changed, 488 insertions(+), 396 deletions(-) diff --git a/packages/shorebird_cli/lib/src/code_push_client_wrapper.dart b/packages/shorebird_cli/lib/src/code_push_client_wrapper.dart index 6a35b554..cc7765f6 100644 --- a/packages/shorebird_cli/lib/src/code_push_client_wrapper.dart +++ b/packages/shorebird_cli/lib/src/code_push_client_wrapper.dart @@ -328,6 +328,71 @@ aab artifact already exists, continuing...''', createArtifactProgress.complete(); } + Future createAndroidArchiveReleaseArtifacts({ + required int releaseId, + required String platform, + required String aarPath, + required String extractedAarDir, + required Map architectures, + }) async { + final createArtifactProgress = logger.progress('Creating artifacts'); + + for (final archMetadata in architectures.values) { + final artifactPath = p.join( + extractedAarDir, + 'jni', + archMetadata.path, + 'libapp.so', + ); + final artifact = File(artifactPath); + final hash = sha256.convert(await artifact.readAsBytes()).toString(); + logger.detail('Creating artifact for $artifactPath'); + + try { + await codePushClient.createReleaseArtifact( + releaseId: releaseId, + artifactPath: artifact.path, + arch: archMetadata.arch, + platform: platform, + hash: hash, + ); + } on CodePushConflictException catch (_) { + // Newlines are due to how logger.info interacts with logger.progress. + logger.info( + ''' + +${archMetadata.arch} artifact already exists, continuing...''', + ); + } catch (error) { + createArtifactProgress.fail('Error uploading ${artifact.path}: $error'); + exit(ExitCode.software.code); + } + } + + try { + logger.detail('Creating artifact for $aarPath'); + await codePushClient.createReleaseArtifact( + releaseId: releaseId, + artifactPath: aarPath, + arch: 'aar', + platform: platform, + hash: sha256.convert(await File(aarPath).readAsBytes()).toString(), + ); + } on CodePushConflictException catch (_) { + // Newlines are due to how logger.info interacts with logger.progress. + logger.info( + ''' + +aar artifact already exists, continuing...''', + ); + } catch (error) { + createArtifactProgress.fail('Error uploading $aarPath: $error'); + exit(ExitCode.software.code); + } + + createArtifactProgress.complete(); + } + @visibleForTesting Future createPatch({required int releaseId}) async { final createPatchProgress = logger.progress('Creating patch'); diff --git a/packages/shorebird_cli/lib/src/commands/release/release_aar_command.dart b/packages/shorebird_cli/lib/src/commands/release/release_aar_command.dart index 0687a009..101d3664 100644 --- a/packages/shorebird_cli/lib/src/commands/release/release_aar_command.dart +++ b/packages/shorebird_cli/lib/src/commands/release/release_aar_command.dart @@ -2,11 +2,8 @@ import 'dart:async'; import 'dart:io'; import 'package:archive/archive_io.dart'; -import 'package:collection/collection.dart'; -import 'package:crypto/crypto.dart'; import 'package:mason_logger/mason_logger.dart'; -import 'package:path/path.dart' as p; -import 'package:shorebird_cli/src/auth/auth.dart'; +import 'package:shorebird_cli/src/code_push_client_wrapper.dart'; import 'package:shorebird_cli/src/command.dart'; import 'package:shorebird_cli/src/config/config.dart'; import 'package:shorebird_cli/src/logger.dart'; @@ -18,7 +15,6 @@ import 'package:shorebird_cli/src/shorebird_environment.dart'; import 'package:shorebird_cli/src/shorebird_java_mixin.dart'; import 'package:shorebird_cli/src/shorebird_release_version_mixin.dart'; import 'package:shorebird_cli/src/shorebird_validation_mixin.dart'; -import 'package:shorebird_code_push_client/shorebird_code_push_client.dart'; /// {@template release_aar_command} /// `shorebird release aar` @@ -35,12 +31,9 @@ class ReleaseAarCommand extends ShorebirdCommand ShorebirdArtifactMixin { /// {@macro release_aar_command} ReleaseAarCommand({ - super.buildCodePushClient, super.validators, - HashFunction? hashFn, UnzipFn? unzipFn, - }) : _hashFn = hashFn ?? ((m) => sha256.convert(m).toString()), - _unzipFn = unzipFn ?? extractFileToDisk { + }) : _unzipFn = unzipFn ?? extractFileToDisk { argParser ..addOption( 'release-version', @@ -78,7 +71,6 @@ Shorebird saves the compiled Dart code from your application in order to make smaller updates to your app. '''; - final HashFunction _hashFn; final UnzipFn _unzipFn; @override @@ -98,10 +90,16 @@ make smaller updates to your app. return ExitCode.config.code; } + const platformName = 'android'; final flavor = results['flavor'] as String?; final buildNumber = results['build-number'] as String; final releaseVersion = results['release-version'] as String; final buildProgress = logger.progress('Building aar'); + + final shorebirdYaml = ShorebirdEnvironment.getShorebirdYaml()!; + final appId = shorebirdYaml.getAppId(flavor: flavor); + final app = await codePushClientWrapper.getApp(appId: appId); + try { await buildAar(buildNumber: buildNumber, flavor: flavor); } on ProcessException catch (error) { @@ -111,41 +109,24 @@ make smaller updates to your app. buildProgress.complete(); - final shorebirdYaml = ShorebirdEnvironment.getShorebirdYaml()!; - final codePushClient = buildCodePushClient( - httpClient: auth.client, - hostedUri: ShorebirdEnvironment.hostedUri, + final existingRelease = await codePushClientWrapper.maybeGetRelease( + appId: appId, + releaseVersion: releaseVersion, ); - - late final List apps; - final fetchAppsProgress = logger.progress('Fetching apps'); - try { - apps = (await codePushClient.getApps()) - .map((a) => App(id: a.appId, displayName: a.displayName)) - .toList(); - fetchAppsProgress.complete(); - } catch (error) { - fetchAppsProgress.fail('$error'); - return ExitCode.software.code; - } - - final appId = shorebirdYaml.getAppId(flavor: flavor); - final app = apps.firstWhereOrNull((a) => a.id == appId); - if (app == null) { + if (existingRelease != null) { logger.err( ''' -Could not find app with id: "$appId". -Did you forget to run "shorebird init"?''', +It looks like you have an existing release for version ${lightCyan.wrap(releaseVersion)}. +Please bump your version number and try again.''', ); return ExitCode.software.code; } - const platformName = 'android'; final archNames = architectures.keys.map( (arch) => arch.name, ); final summary = [ - '''šŸ“± App: ${lightCyan.wrap(app.displayName)} ${lightCyan.wrap('(${app.id})')}''', + '''šŸ“± App: ${lightCyan.wrap(app.displayName)} ${lightCyan.wrap('(${app.appId})')}''', if (flavor != null) 'šŸ§ Flavor: ${lightCyan.wrap(flavor)}', 'šŸ“¦ Release Version: ${lightCyan.wrap(releaseVersion)}', '''šŸ•¹ļø Platform: ${lightCyan.wrap(platformName)} ${lightCyan.wrap('(${archNames.join(', ')})')}''', @@ -169,110 +150,42 @@ ${summary.join('\n')} } } - late final List releases; - final fetchReleasesProgress = logger.progress('Fetching releases'); + final flutterRevisionProgress = logger.progress( + 'Fetching Flutter revision', + ); + final String shorebirdFlutterRevision; try { - releases = await codePushClient.getReleases(appId: app.id); - fetchReleasesProgress.complete(); + shorebirdFlutterRevision = await getShorebirdFlutterRevision(); + flutterRevisionProgress.complete(); } catch (error) { - fetchReleasesProgress.fail('$error'); + flutterRevisionProgress.fail('$error'); return ExitCode.software.code; } - var release = releases.firstWhereOrNull((r) => r.version == releaseVersion); - if (release == null) { - final flutterRevisionProgress = logger.progress( - 'Fetching Flutter revision', - ); - final String shorebirdFlutterRevision; - try { - shorebirdFlutterRevision = await getShorebirdFlutterRevision(); - flutterRevisionProgress.complete(); - } catch (error) { - flutterRevisionProgress.fail('$error'); - return ExitCode.software.code; - } - - final createReleaseProgress = logger.progress('Creating release'); - try { - release = await codePushClient.createRelease( - appId: app.id, - version: releaseVersion, - flutterRevision: shorebirdFlutterRevision, - ); - createReleaseProgress.complete(); - } catch (error) { - createReleaseProgress.fail('$error'); - return ExitCode.software.code; - } - } - - final createArtifactProgress = logger.progress('Creating artifacts'); + final release = await codePushClientWrapper.createRelease( + appId: appId, + version: releaseVersion, + flutterRevision: shorebirdFlutterRevision, + ); + final extractAarProgress = logger.progress('Creating artifacts'); final extractedAarDir = await extractAar( packageName: androidPackageName!, buildNumber: buildNumber, unzipFn: _unzipFn, ); + extractAarProgress.complete(); - for (final archMetadata in architectures.values) { - final artifactPath = p.join( - extractedAarDir, - 'jni', - archMetadata.path, - 'libapp.so', - ); - final artifact = File(artifactPath); - final hash = _hashFn(await artifact.readAsBytes()); - logger.detail('Creating artifact for $artifactPath'); - - try { - await codePushClient.createReleaseArtifact( - releaseId: release.id, - artifactPath: artifact.path, - arch: archMetadata.arch, - platform: platformName, - hash: hash, - ); - } on CodePushConflictException catch (_) { - // Newlines are due to how logger.info interacts with logger.progress. - logger.info( - ''' - -${archMetadata.arch} artifact already exists, continuing...''', - ); - } catch (error) { - createArtifactProgress.fail('Error uploading ${artifact.path}: $error'); - return ExitCode.software.code; - } - } - - final aarPath = aarArtifactPath( - packageName: androidPackageName!, - buildNumber: buildNumber, + await codePushClientWrapper.createAndroidArchiveReleaseArtifacts( + releaseId: release.id, + platform: platformName, + aarPath: aarArtifactPath( + packageName: androidPackageName!, + buildNumber: buildNumber, + ), + extractedAarDir: extractedAarDir, + architectures: architectures, ); - try { - logger.detail('Creating artifact for $aarPath'); - await codePushClient.createReleaseArtifact( - releaseId: release.id, - artifactPath: aarPath, - arch: 'aar', - platform: platformName, - hash: _hashFn(await File(aarPath).readAsBytes()), - ); - } on CodePushConflictException catch (_) { - // Newlines are due to how logger.info interacts with logger.progress. - logger.info( - ''' - -aar artifact already exists, continuing...''', - ); - } catch (error) { - createArtifactProgress.fail('Error uploading $aarPath: $error'); - return ExitCode.software.code; - } - - createArtifactProgress.complete(); logger ..success('\nāœ… Published Release!') diff --git a/packages/shorebird_cli/lib/src/shorebird_artifact_mixin.dart b/packages/shorebird_cli/lib/src/shorebird_artifact_mixin.dart index 7aab0594..f8907fa5 100644 --- a/packages/shorebird_cli/lib/src/shorebird_artifact_mixin.dart +++ b/packages/shorebird_cli/lib/src/shorebird_artifact_mixin.dart @@ -10,6 +10,7 @@ mixin ShorebirdArtifactMixin on ShorebirdCommand { required String buildNumber, }) => p.joinAll([ + Directory.current.path, 'build', 'host', 'outputs', @@ -44,18 +45,20 @@ mixin ShorebirdArtifactMixin on ShorebirdCommand { packageName: packageName, buildNumber: buildNumber, ); - final zipPath = p.join(aarDirectory, 'flutter_release-$buildNumber.zip'); + + final zipDir = Directory.systemTemp.createTempSync(); + final zipPath = p.join(zipDir.path, 'flutter_release-$buildNumber.zip'); logger.detail('Extracting $aarPath to $zipPath'); // Copy the .aar file to a .zip file so package:archive knows how to read it File(aarPath).copySync(zipPath); - final extractedAarDir = p.join( + final extractedZipDir = p.join( aarDirectory, 'flutter_release-$buildNumber', ); // Unzip the .zip file to a directory so we can read the .so files - await unzipFn(zipPath, extractedAarDir); - return extractedAarDir; + await unzipFn(zipPath, extractedZipDir); + return extractedZipDir; } /// Finds the most recently-edited app.dill file in the .dart_tool directory. diff --git a/packages/shorebird_cli/test/src/code_push_client_wrapper_test.dart b/packages/shorebird_cli/test/src/code_push_client_wrapper_test.dart index 3fc657bd..a8d0b69b 100644 --- a/packages/shorebird_cli/test/src/code_push_client_wrapper_test.dart +++ b/packages/shorebird_cli/test/src/code_push_client_wrapper_test.dart @@ -115,9 +115,16 @@ void main() { late Logger logger; late Progress progress; late CodePushClientWrapper codePushClientWrapper; + late Platform platform; R runWithOverrides(R Function() body) { - return runScoped(body, values: {loggerRef.overrideWith(() => logger)}); + return runScoped( + body, + values: { + loggerRef.overrideWith(() => logger), + platformRef.overrideWith(() => platform), + }, + ); } setUpAll(setExitFunctionForTests); @@ -127,6 +134,7 @@ void main() { setUp(() { codePushClient = _MockCodePushClient(); logger = _MockLogger(); + platform = _MockPlatform(); progress = _MockProgress(); codePushClientWrapper = runWithOverrides( @@ -134,6 +142,16 @@ void main() { ); when(() => logger.progress(any())).thenReturn(progress); + when(() => platform.script).thenReturn( + Uri.file( + p.join( + Directory.systemTemp.createTempSync().path, + 'bin', + 'cache', + 'shorebird.snapshot', + ), + ), + ); }); group('app', () { @@ -948,6 +966,261 @@ void main() { verifyNever(() => progress.fail(any())); }); }); + + group('createAndroidArchiveReleaseArtifacts', () { + const buildNumber = '1.0'; + + final aarDir = p.join( + 'build', + 'host', + 'outputs', + 'repo', + 'com', + 'example', + 'my_flutter_module', + 'flutter_release', + buildNumber, + ); + final aarPath = p.join(aarDir, 'flutter_release-$buildNumber.aar'); + final extractedAarPath = p.join(aarDir, 'flutter_release-$buildNumber'); + + Directory setUpTempDir({String? flavor}) { + final tempDir = Directory.systemTemp.createTempSync(); + for (final archMetadata + in ShorebirdBuildMixin.allAndroidArchitectures.values) { + final artifactPath = p.join( + tempDir.path, + extractedAarPath, + 'jni', + archMetadata.path, + 'libapp.so', + ); + File(artifactPath).createSync(recursive: true); + } + File(p.join(tempDir.path, aarPath)).createSync(recursive: true); + return tempDir; + } + + setUp(() { + when( + () => codePushClient.createReleaseArtifact( + artifactPath: any(named: 'artifactPath'), + releaseId: any(named: 'releaseId'), + arch: any(named: 'arch'), + platform: any(named: 'platform'), + hash: any(named: 'hash'), + ), + ).thenAnswer((_) async => {}); + }); + + test('exits with code 70 when artifact creation fails', () async { + const error = 'something went wrong'; + when( + () => codePushClient.createReleaseArtifact( + artifactPath: any(named: 'artifactPath'), + releaseId: any(named: 'releaseId'), + arch: any(named: 'arch'), + platform: any(named: 'platform'), + hash: any(named: 'hash'), + ), + ).thenThrow(error); + final tempDir = setUpTempDir(); + + await IOOverrides.runZoned( + () async => expectLater( + () async => runWithOverrides( + () async => + codePushClientWrapper.createAndroidArchiveReleaseArtifacts( + releaseId: releaseId, + platform: platformName, + aarPath: p.join(tempDir.path, aarPath), + extractedAarDir: p.join(tempDir.path, extractedAarPath), + architectures: ShorebirdBuildMixin.allAndroidArchitectures, + ), + ), + exitsWithCode(ExitCode.software), + ), + getCurrentDirectory: () => tempDir, + ); + + verify(() => progress.fail(any(that: contains(error)))).called(1); + }); + + test('exits with code 70 when aar artifact creation fails', () async { + const error = 'something went wrong'; + when( + () => codePushClient.createReleaseArtifact( + artifactPath: any(named: 'artifactPath', that: endsWith('aar')), + releaseId: any(named: 'releaseId'), + arch: any(named: 'arch'), + platform: any(named: 'platform'), + hash: any(named: 'hash'), + ), + ).thenThrow(error); + final tempDir = setUpTempDir(); + + await IOOverrides.runZoned( + () async => expectLater( + () async => runWithOverrides( + () async => + codePushClientWrapper.createAndroidArchiveReleaseArtifacts( + releaseId: releaseId, + platform: platformName, + aarPath: p.join(tempDir.path, aarPath), + extractedAarDir: p.join(tempDir.path, extractedAarPath), + architectures: ShorebirdBuildMixin.allAndroidArchitectures, + ), + ), + exitsWithCode(ExitCode.software), + ), + getCurrentDirectory: () => tempDir, + ); + + verify(() => progress.fail(any(that: contains(error)))).called(1); + }); + + test('logs message when uploading release artifact that already exists', + () async { + const error = 'something went wrong'; + when( + () => codePushClient.createReleaseArtifact( + artifactPath: any(named: 'artifactPath'), + releaseId: any(named: 'releaseId'), + arch: any(named: 'arch'), + platform: any(named: 'platform'), + hash: any(named: 'hash'), + ), + ).thenThrow(const CodePushConflictException(message: error)); + final tempDir = setUpTempDir(); + + await runWithOverrides( + () async => IOOverrides.runZoned( + () async => + codePushClientWrapper.createAndroidArchiveReleaseArtifacts( + releaseId: releaseId, + platform: platformName, + aarPath: p.join(tempDir.path, aarPath), + extractedAarDir: p.join(tempDir.path, extractedAarPath), + architectures: ShorebirdBuildMixin.allAndroidArchitectures, + ), + getCurrentDirectory: () => tempDir, + ), + ); + + // 1 for each arch, 1 for the aab + final numArtifactsUploaded = + ShorebirdBuildMixin.allAndroidArchitectures.values.length + 1; + verify( + () => logger.info(any(that: contains('already exists'))), + ).called(numArtifactsUploaded); + verifyNever(() => progress.fail(error)); + }); + + test('logs message when uploading aar that already exists', () async { + const error = 'something went wrong'; + when( + () => codePushClient.createReleaseArtifact( + artifactPath: any(named: 'artifactPath', that: endsWith('.aar')), + releaseId: any(named: 'releaseId'), + arch: any(named: 'arch'), + platform: any(named: 'platform'), + hash: any(named: 'hash'), + ), + ).thenThrow(const CodePushConflictException(message: error)); + final tempDir = setUpTempDir(); + + await runWithOverrides( + () async => IOOverrides.runZoned( + () async => + codePushClientWrapper.createAndroidArchiveReleaseArtifacts( + releaseId: releaseId, + platform: platformName, + aarPath: p.join(tempDir.path, aarPath), + extractedAarDir: p.join(tempDir.path, extractedAarPath), + architectures: ShorebirdBuildMixin.allAndroidArchitectures, + ), + getCurrentDirectory: () => tempDir, + ), + ); + + verify( + () => logger.info( + any(that: contains('aar artifact already exists, continuing...')), + ), + ).called(1); + verifyNever(() => progress.fail(error)); + }); + + test('completes successfully when all artifacts are created', () async { + when( + () => codePushClient.createReleaseArtifact( + artifactPath: any(named: 'artifactPath'), + releaseId: any(named: 'releaseId'), + arch: any(named: 'arch'), + platform: any(named: 'platform'), + hash: any(named: 'hash'), + ), + ).thenAnswer((_) async => {}); + final tempDir = setUpTempDir(); + + await runWithOverrides( + () async => IOOverrides.runZoned( + () async => + codePushClientWrapper.createAndroidArchiveReleaseArtifacts( + releaseId: releaseId, + platform: platformName, + aarPath: p.join(tempDir.path, aarPath), + extractedAarDir: p.join(tempDir.path, extractedAarPath), + architectures: ShorebirdBuildMixin.allAndroidArchitectures, + ), + getCurrentDirectory: () => tempDir, + ), + ); + + verify(() => progress.complete()).called(1); + verifyNever(() => progress.fail(any())); + }); + + test('completes succesfully when a flavor is provided', () async { + const flavorName = 'myFlavor'; + when( + () => codePushClient.createReleaseArtifact( + artifactPath: any(named: 'artifactPath'), + releaseId: any(named: 'releaseId'), + arch: any(named: 'arch'), + platform: any(named: 'platform'), + hash: any(named: 'hash'), + ), + ).thenAnswer((_) async => {}); + final tempDir = setUpTempDir(flavor: flavorName); + + await runWithOverrides( + () async => IOOverrides.runZoned( + () async => + codePushClientWrapper.createAndroidArchiveReleaseArtifacts( + releaseId: releaseId, + platform: platformName, + aarPath: p.join(tempDir.path, aarPath), + extractedAarDir: p.join(tempDir.path, extractedAarPath), + architectures: ShorebirdBuildMixin.allAndroidArchitectures, + ), + getCurrentDirectory: () => tempDir, + ), + ); + + verify( + () => codePushClient.createReleaseArtifact( + artifactPath: any(named: 'artifactPath'), + releaseId: releaseId, + arch: any(named: 'arch'), + platform: platformName, + hash: any(named: 'hash'), + ), + ).called(ShorebirdBuildMixin.allAndroidArchitectures.length + 1); + verify(() => progress.complete()).called(1); + verifyNever(() => progress.fail(any())); + }); + }); }); group('patch', () { diff --git a/packages/shorebird_cli/test/src/commands/patch/patch_aar_command_test.dart b/packages/shorebird_cli/test/src/commands/patch/patch_aar_command_test.dart index dc00d7f0..72828679 100644 --- a/packages/shorebird_cli/test/src/commands/patch/patch_aar_command_test.dart +++ b/packages/shorebird_cli/test/src/commands/patch/patch_aar_command_test.dart @@ -157,6 +157,7 @@ flutter: void setUpTempArtifacts(Directory dir) { final aarDir = p.join( + dir.path, 'build', 'host', 'outputs', diff --git a/packages/shorebird_cli/test/src/commands/release/release_aar_command_test.dart b/packages/shorebird_cli/test/src/commands/release/release_aar_command_test.dart index 2d7fb907..c415403a 100644 --- a/packages/shorebird_cli/test/src/commands/release/release_aar_command_test.dart +++ b/packages/shorebird_cli/test/src/commands/release/release_aar_command_test.dart @@ -5,10 +5,13 @@ import 'package:http/http.dart' as http; import 'package:mason_logger/mason_logger.dart'; import 'package:mocktail/mocktail.dart'; import 'package:path/path.dart' as p; +import 'package:platform/platform.dart'; import 'package:scoped/scoped.dart'; import 'package:shorebird_cli/src/auth/auth.dart'; +import 'package:shorebird_cli/src/code_push_client_wrapper.dart'; import 'package:shorebird_cli/src/commands/commands.dart'; import 'package:shorebird_cli/src/logger.dart'; +import 'package:shorebird_cli/src/platform.dart'; import 'package:shorebird_cli/src/shorebird_build_mixin.dart'; import 'package:shorebird_cli/src/shorebird_process.dart'; import 'package:shorebird_cli/src/validators/validators.dart'; @@ -23,11 +26,14 @@ class _MockAuth extends Mock implements Auth {} class _MockLogger extends Mock implements Logger {} +class _MockPlatform extends Mock implements Platform {} + class _MockProgress extends Mock implements Progress {} class _MockProcessResult extends Mock implements ShorebirdProcessResult {} -class _MockCodePushClient extends Mock implements CodePushClient {} +class _MockCodePushClientWrapper extends Mock + implements CodePushClientWrapper {} class _MockShorebirdFlutterValidator extends Mock implements ShorebirdFlutterValidator {} @@ -53,18 +59,7 @@ void main() { displayName: '1.2.3+1', ); - const arch = 'aarch64'; const platformName = 'android'; - const releaseArtifact = ReleaseArtifact( - id: 0, - releaseId: 0, - arch: arch, - platform: platformName, - hash: '#', - size: 42, - url: 'https://example.com', - ); - const buildNumber = '1.0'; const noModulePubspecYamlContent = ''' name: example @@ -93,13 +88,14 @@ flutter: late ArgResults argResults; late http.Client httpClient; late Auth auth; + late CodePushClientWrapper codePushClientWrapper; + late Directory shorebirdRoot; + late Platform platform; late Progress progress; late Logger logger; late ShorebirdProcessResult flutterBuildProcessResult; late ShorebirdProcessResult flutterRevisionProcessResult; - late CodePushClient codePushClient; late ReleaseAarCommand command; - late Uri? capturedHostedUri; late ShorebirdFlutterValidator flutterValidator; late ShorebirdProcess shorebirdProcess; @@ -108,7 +104,9 @@ flutter: body, values: { authRef.overrideWith(() => auth), - loggerRef.overrideWith(() => logger) + codePushClientWrapperRef.overrideWith(() => codePushClientWrapper), + loggerRef.overrideWith(() => logger), + platformRef.overrideWith(() => platform), }, ); } @@ -128,6 +126,7 @@ flutter: void setUpTempArtifacts(Directory dir) { final aarDir = p.join( + dir.path, 'build', 'host', 'outputs', @@ -157,13 +156,15 @@ flutter: argResults = _MockArgResults(); httpClient = _MockHttpClient(); auth = _MockAuth(); + codePushClientWrapper = _MockCodePushClientWrapper(); + platform = _MockPlatform(); progress = _MockProgress(); logger = _MockLogger(); flutterBuildProcessResult = _MockProcessResult(); flutterRevisionProcessResult = _MockProcessResult(); - codePushClient = _MockCodePushClient(); flutterValidator = _MockShorebirdFlutterValidator(); shorebirdProcess = _MockShorebirdProcess(); + shorebirdRoot = Directory.systemTemp.createTempSync(); registerFallbackValue(shorebirdProcess); @@ -175,6 +176,17 @@ flutter: when(() => logger.confirm(any())).thenReturn(true); when(() => logger.progress(any())).thenReturn(progress); + when(() => platform.script).thenReturn( + Uri.file( + p.join( + shorebirdRoot.path, + 'bin', + 'cache', + 'shorebird.snapshot', + ), + ), + ); + when(() => flutterBuildProcessResult.exitCode) .thenReturn(ExitCode.success.code); @@ -204,39 +216,35 @@ flutter: ).thenAnswer((_) async => flutterRevisionProcessResult); when( - () => codePushClient.getApps(), - ).thenAnswer((_) async => [appMetadata]); + () => codePushClientWrapper.getApp(appId: any(named: 'appId')), + ).thenAnswer((_) async => appMetadata); when( - () => codePushClient.getReleases(appId: any(named: 'appId')), - ).thenAnswer((_) async => [release]); + () => codePushClientWrapper.maybeGetRelease( + appId: any(named: 'appId'), + releaseVersion: any(named: 'releaseVersion'), + ), + ).thenAnswer((_) async => null); when( - () => codePushClient.createRelease( + () => codePushClientWrapper.createRelease( appId: any(named: 'appId'), version: any(named: 'version'), flutterRevision: any(named: 'flutterRevision'), ), ).thenAnswer((_) async => release); when( - () => codePushClient.createReleaseArtifact( - artifactPath: any(named: 'artifactPath'), + () => codePushClientWrapper.createAndroidArchiveReleaseArtifacts( releaseId: any(named: 'releaseId'), - arch: any(named: 'arch'), platform: any(named: 'platform'), - hash: any(named: 'hash'), + aarPath: any(named: 'aarPath'), + extractedAarDir: any(named: 'extractedAarDir'), + architectures: any(named: 'architectures'), ), - ).thenAnswer((_) async => releaseArtifact); + ).thenAnswer((_) async => {}); when(() => flutterValidator.validate(any())).thenAnswer((_) async => []); command = runWithOverrides( () => ReleaseAarCommand( - buildCodePushClient: ({ - required http.Client httpClient, - Uri? hostedUri, - }) { - capturedHostedUri = hostedUri; - return codePushClient; - }, unzipFn: (_, __) async {}, validators: [flutterValidator], ), @@ -331,36 +339,25 @@ flutter: .called(1); }); - test('throws error when fetching apps fails.', () async { - const error = 'something went wrong'; - when(() => codePushClient.getApps()).thenThrow(error); - final tempDir = setUpTempDir(); - - final exitCode = await IOOverrides.runZoned( - () => runWithOverrides(command.run), - getCurrentDirectory: () => tempDir, - ); - - verify(() => progress.fail(error)).called(1); - expect(exitCode, ExitCode.software.code); - }); - - test('throws error when app does not exist.', () async { + test('throws error when existing releases exists.', () async { when( - () => logger.prompt(any(), defaultValue: any(named: 'defaultValue')), - ).thenReturn(appDisplayName); - when(() => codePushClient.getApps()).thenAnswer((_) async => []); + () => codePushClientWrapper.maybeGetRelease( + appId: any(named: 'appId'), + releaseVersion: any(named: 'releaseVersion'), + ), + ).thenAnswer((_) async => release); final tempDir = setUpTempDir(); + setUpTempArtifacts(tempDir); + final exitCode = await IOOverrides.runZoned( () => runWithOverrides(command.run), getCurrentDirectory: () => tempDir, ); + verify( - () => logger.err( - ''' -Could not find app with id: "$appId". -Did you forget to run "shorebird init"?''', - ), + () => logger.err(''' +It looks like you have an existing release for version ${lightCyan.wrap(versionName)}. +Please bump your version number and try again.'''), ).called(1); expect(exitCode, ExitCode.software.code); }); @@ -374,7 +371,6 @@ Did you forget to run "shorebird init"?''', ), ).thenAnswer((_) => '1.0.0'); final tempDir = setUpTempDir(); - // setUpTempArtifacts(tempDir); final exitCode = await IOOverrides.runZoned( () => runWithOverrides(command.run), getCurrentDirectory: () => tempDir, @@ -383,29 +379,11 @@ Did you forget to run "shorebird init"?''', verify(() => logger.info('Aborting.')).called(1); }); - test('throws error when fetching releases fails.', () async { - const error = 'something went wrong'; - when( - () => codePushClient.getReleases(appId: any(named: 'appId')), - ).thenThrow(error); - final tempDir = setUpTempDir(); - final exitCode = await IOOverrides.runZoned( - () => runWithOverrides(command.run), - getCurrentDirectory: () => tempDir, - ); - verify(() => progress.fail(error)).called(1); - expect(exitCode, ExitCode.software.code); - }); - test('throws error when unable to detect flutter revision', () async { const error = 'oops'; when(() => flutterRevisionProcessResult.exitCode).thenReturn(1); when(() => flutterRevisionProcessResult.stderr).thenReturn(error); - when( - () => codePushClient.getReleases(appId: any(named: 'appId')), - ).thenAnswer((_) async => []); final tempDir = setUpTempDir(); - // setUpTempArtifacts(tempDir); final exitCode = await IOOverrides.runZoned( () => runWithOverrides(command.run), getCurrentDirectory: () => tempDir, @@ -418,175 +396,6 @@ Did you forget to run "shorebird init"?''', ).called(1); }); - test('throws error when creating release fails.', () async { - const error = 'something went wrong'; - when( - () => codePushClient.getReleases(appId: any(named: 'appId')), - ).thenAnswer((_) async => []); - when( - () => codePushClient.createRelease( - appId: any(named: 'appId'), - version: any(named: 'version'), - flutterRevision: any(named: 'flutterRevision'), - displayName: any(named: 'displayName'), - ), - ).thenThrow(error); - final tempDir = setUpTempDir(); - final exitCode = await IOOverrides.runZoned( - () => runWithOverrides(command.run), - getCurrentDirectory: () => tempDir, - ); - verify(() => progress.fail(error)).called(1); - expect(exitCode, ExitCode.software.code); - }); - - test('logs message when uploading release artifact that already exists.', - () async { - const error = 'something went wrong'; - when( - () => codePushClient.getReleases(appId: any(named: 'appId')), - ).thenAnswer((_) async => []); - when( - () => codePushClient.createReleaseArtifact( - artifactPath: any(named: 'artifactPath'), - releaseId: any(named: 'releaseId'), - arch: any(named: 'arch'), - platform: any(named: 'platform'), - hash: any(named: 'hash'), - ), - ).thenThrow(const CodePushConflictException(message: error)); - final tempDir = setUpTempDir(); - setUpTempArtifacts(tempDir); - - final exitCode = await IOOverrides.runZoned( - () => runWithOverrides(command.run), - getCurrentDirectory: () => tempDir, - ); - - // 1 for each arch, 1 for the aar - final numArtifactsUploaded = Arch.values.length + 1; - verify( - () => codePushClient.createRelease( - appId: appId, - version: versionName, - flutterRevision: flutterRevision, - ), - ).called(1); - verify( - () => logger.info(any(that: contains('already exists'))), - ).called(numArtifactsUploaded); - verifyNever(() => progress.fail(error)); - expect(exitCode, ExitCode.success.code); - }); - - test('logs message when uploading aar that already exists.', () async { - const error = 'something went wrong'; - when( - () => codePushClient.getReleases(appId: any(named: 'appId')), - ).thenAnswer((_) async => []); - when( - () => codePushClient.createReleaseArtifact( - artifactPath: any(named: 'artifactPath', that: endsWith('.aar')), - releaseId: any(named: 'releaseId'), - arch: any(named: 'arch'), - platform: any(named: 'platform'), - hash: any(named: 'hash'), - ), - ).thenThrow(const CodePushConflictException(message: error)); - final tempDir = setUpTempDir(); - setUpTempArtifacts(tempDir); - - final exitCode = await IOOverrides.runZoned( - () => runWithOverrides(command.run), - getCurrentDirectory: () => tempDir, - ); - - verify( - () => logger.info( - any(that: contains('aar artifact already exists, continuing...')), - ), - ).called(1); - verify( - () => codePushClient.createRelease( - appId: appId, - version: versionName, - flutterRevision: flutterRevision, - ), - ).called(1); - verifyNever(() => progress.fail(error)); - expect(exitCode, ExitCode.success.code); - }); - - test('throws error when uploading release artifact fails.', () async { - const error = 'something went wrong'; - when( - () => codePushClient.getReleases(appId: any(named: 'appId')), - ).thenAnswer((_) async => []); - when( - () => codePushClient.createReleaseArtifact( - artifactPath: any(named: 'artifactPath'), - releaseId: any(named: 'releaseId'), - arch: any(named: 'arch'), - platform: any(named: 'platform'), - hash: any(named: 'hash'), - ), - ).thenThrow(error); - final tempDir = setUpTempDir(); - setUpTempArtifacts(tempDir); - - final exitCode = await IOOverrides.runZoned( - () => runWithOverrides(command.run), - getCurrentDirectory: () => tempDir, - ); - - verify( - () => progress - .fail(any(that: stringContainsInOrder(['libapp.so', error]))), - ).called(1); - verify( - () => codePushClient.createRelease( - appId: appId, - version: versionName, - flutterRevision: flutterRevision, - ), - ).called(1); - expect(exitCode, ExitCode.software.code); - }); - - test('throws error when uploading aar fails', () async { - const error = 'something went wrong'; - when( - () => codePushClient.getReleases(appId: any(named: 'appId')), - ).thenAnswer((_) async => []); - when( - () => codePushClient.createReleaseArtifact( - artifactPath: any(named: 'artifactPath', that: endsWith('.aar')), - releaseId: any(named: 'releaseId'), - arch: any(named: 'arch'), - platform: any(named: 'platform'), - hash: any(named: 'hash'), - ), - ).thenThrow(error); - final tempDir = setUpTempDir(); - setUpTempArtifacts(tempDir); - - final exitCode = await IOOverrides.runZoned( - () => runWithOverrides(command.run), - getCurrentDirectory: () => tempDir, - ); - - verify( - () => progress.fail( - any( - that: stringContainsInOrder( - ['flutter_release-$buildNumber.aar', error], - ), - ), - ), - ).called(1); - expect(exitCode, ExitCode.software.code); - }); - test('does not prompt for confirmation when --force is used', () async { when(() => argResults['force']).thenReturn(true); final tempDir = setUpTempDir(); @@ -597,9 +406,8 @@ Did you forget to run "shorebird init"?''', getCurrentDirectory: () => tempDir, ); - verify(() => logger.success('\nāœ… Published Release!')).called(1); expect(exitCode, ExitCode.success.code); - expect(capturedHostedUri, isNull); + verify(() => logger.success('\nāœ… Published Release!')).called(1); verifyNever( () => logger.prompt(any(), defaultValue: any(named: 'defaultValue')), ); @@ -608,22 +416,33 @@ Did you forget to run "shorebird init"?''', test('succeeds when release is successful', () async { final tempDir = setUpTempDir(); setUpTempArtifacts(tempDir); + final exitCode = await IOOverrides.runZoned( () => runWithOverrides(command.run), getCurrentDirectory: () => tempDir, ); + + expect(exitCode, ExitCode.success.code); verify(() => logger.success('\nāœ… Published Release!')).called(1); verify( - () => codePushClient.createReleaseArtifact( - artifactPath: any(named: 'artifactPath', that: endsWith('.aar')), - releaseId: any(named: 'releaseId'), - arch: 'aar', - platform: 'android', - hash: any(named: 'hash'), + () => codePushClientWrapper.createAndroidArchiveReleaseArtifacts( + releaseId: release.id, + platform: platformName, + aarPath: any( + named: 'aarPath', + that: endsWith( + '/build/host/outputs/repo/com/example/my_flutter_module/flutter_release/1.0/flutter_release-1.0.aar', + ), + ), + extractedAarDir: any( + named: 'extractedAarDir', + that: endsWith( + 'build/host/outputs/repo/com/example/my_flutter_module/flutter_release/1.0/flutter_release-1.0', + ), + ), + architectures: any(named: 'architectures'), ), ).called(1); - expect(exitCode, ExitCode.success.code); - expect(capturedHostedUri, isNull); }); test( @@ -632,29 +451,48 @@ Did you forget to run "shorebird init"?''', const flavor = 'development'; when(() => argResults['flavor']).thenReturn(flavor); final tempDir = setUpTempDir(); + setUpTempArtifacts(tempDir); File( p.join(tempDir.path, 'shorebird.yaml'), ).writeAsStringSync(''' app_id: productionAppId flavors: development: $appId'''); - setUpTempArtifacts(tempDir); + final exitCode = await IOOverrides.runZoned( () => runWithOverrides(command.run), getCurrentDirectory: () => tempDir, ); + + expect(exitCode, ExitCode.success.code); verify(() => logger.success('\nāœ… Published Release!')).called(1); + final capturedArgs = verify( + () => shorebirdProcess.run( + 'flutter', + captureAny(), + runInShell: true, + ), + ).captured.first as List; + expect(capturedArgs, contains('--flavor=$flavor')); verify( - () => codePushClient.createReleaseArtifact( - artifactPath: any(named: 'artifactPath', that: endsWith('.aar')), - releaseId: any(named: 'releaseId'), - arch: 'aar', - platform: 'android', - hash: any(named: 'hash'), + () => codePushClientWrapper.createAndroidArchiveReleaseArtifacts( + releaseId: release.id, + platform: platformName, + aarPath: any( + named: 'aarPath', + that: endsWith( + '/build/host/outputs/repo/com/example/my_flutter_module/flutter_release/1.0/flutter_release-1.0.aar', + ), + ), + extractedAarDir: any( + named: 'extractedAarDir', + that: endsWith( + 'build/host/outputs/repo/com/example/my_flutter_module/flutter_release/1.0/flutter_release-1.0', + ), + ), + architectures: any(named: 'architectures'), ), ).called(1); - expect(exitCode, ExitCode.success.code); - expect(capturedHostedUri, isNull); }); test('prints flutter validation warnings', () async { @@ -678,9 +516,8 @@ flavors: getCurrentDirectory: () => tempDir, ); - verify(() => logger.success('\nāœ… Published Release!')).called(1); expect(exitCode, ExitCode.success.code); - expect(capturedHostedUri, isNull); + verify(() => logger.success('\nāœ… Published Release!')).called(1); verify( () => logger.info(any(that: contains('Flutter issue 1'))), ).called(1); @@ -700,11 +537,11 @@ flavors: ); final tempDir = setUpTempDir(); - setUpTempArtifacts(tempDir); final exitCode = await IOOverrides.runZoned( () => runWithOverrides(command.run), getCurrentDirectory: () => tempDir, ); + expect(exitCode, equals(ExitCode.config.code)); verify(() => logger.err('Aborting due to validation errors.')).called(1); });