refactor(shorebird_cli): migrate ReleaseAarCommand to use CodePushClientWrapper (#665)

This commit is contained in:
Bryan Oltman
2023-06-15 16:03:03 -04:00
committed by GitHub
parent 42ededbb2b
commit 13685b3c5d
6 changed files with 488 additions and 396 deletions
@@ -328,6 +328,71 @@ aab artifact already exists, continuing...''',
createArtifactProgress.complete();
}
Future<void> createAndroidArchiveReleaseArtifacts({
required int releaseId,
required String platform,
required String aarPath,
required String extractedAarDir,
required Map<Arch, ArchMetadata> 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<Patch> createPatch({required int releaseId}) async {
final createPatchProgress = logger.progress('Creating patch');
@@ -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<App> 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<Release> 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!')
@@ -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.
@@ -115,9 +115,16 @@ void main() {
late Logger logger;
late Progress progress;
late CodePushClientWrapper codePushClientWrapper;
late Platform platform;
R runWithOverrides<R>(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', () {
@@ -157,6 +157,7 @@ flutter:
void setUpTempArtifacts(Directory dir) {
final aarDir = p.join(
dir.path,
'build',
'host',
'outputs',
@@ -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<String>;
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);
});