diff --git a/packages/shorebird_cli/lib/src/artifact_builder.dart b/packages/shorebird_cli/lib/src/artifact_builder.dart index 7350e2b7..6fc1c33a 100644 --- a/packages/shorebird_cli/lib/src/artifact_builder.dart +++ b/packages/shorebird_cli/lib/src/artifact_builder.dart @@ -6,6 +6,7 @@ import 'package:shorebird_cli/src/logger.dart'; import 'package:shorebird_cli/src/os/operating_system_interface.dart'; import 'package:shorebird_cli/src/platform/platform.dart'; import 'package:shorebird_cli/src/shorebird_android_artifacts.dart'; +import 'package:shorebird_cli/src/shorebird_artifacts.dart'; import 'package:shorebird_cli/src/shorebird_env.dart'; import 'package:shorebird_cli/src/shorebird_process.dart'; @@ -321,4 +322,33 @@ Either run `flutter pub get` manually, or follow the steps in ${link(uri: Uri.pa ); } } + + /// Creates an AOT snapshot of the given [appDillPath] at [outFilePath] and + /// returns the resulting file. + Future buildElfAotSnapshot({ + required String appDillPath, + required String outFilePath, + }) async { + final arguments = [ + '--deterministic', + '--snapshot-kind=app-aot-elf', + '--elf=$outFilePath', + appDillPath, + ]; + + final result = await process.run( + shorebirdArtifacts.getArtifactPath( + artifact: ShorebirdArtifact.genSnapshot, + ), + arguments, + ); + + if (result.exitCode != ExitCode.success.code) { + throw ArtifactBuildException( + 'Failed to create snapshot: ${result.stderr}', + ); + } + + return File(outFilePath); + } } diff --git a/packages/shorebird_cli/lib/src/artifact_manager.dart b/packages/shorebird_cli/lib/src/artifact_manager.dart index e6fe42e2..eac25583 100644 --- a/packages/shorebird_cli/lib/src/artifact_manager.dart +++ b/packages/shorebird_cli/lib/src/artifact_manager.dart @@ -270,4 +270,27 @@ class ArtifactManager { ), ); } + + /// Finds the most recently-edited app.dill file in the .dart_tool directory. + // TODO(bryanoltman): This is an enormous hack – we don't know that this is + // the correct file. + File newestAppDill() { + final projectRoot = shorebirdEnv.getShorebirdProjectRoot()!; + final dartToolBuildDir = Directory( + p.join( + projectRoot.path, + '.dart_tool', + 'flutter_build', + ), + ); + + return dartToolBuildDir + .listSync(recursive: true) + .whereType() + .where((f) => p.basename(f.path) == 'app.dill') + .reduce( + (a, b) => + a.statSync().modified.isAfter(b.statSync().modified) ? a : b, + ); + } } diff --git a/packages/shorebird_cli/lib/src/commands/patch_new/ios_framework_patcher.dart b/packages/shorebird_cli/lib/src/commands/patch_new/ios_framework_patcher.dart new file mode 100644 index 00000000..1e80aeca --- /dev/null +++ b/packages/shorebird_cli/lib/src/commands/patch_new/ios_framework_patcher.dart @@ -0,0 +1,290 @@ +import 'package:crypto/crypto.dart'; +import 'package:mason_logger/mason_logger.dart'; +import 'package:meta/meta.dart'; +import 'package:path/path.dart' as p; +import 'package:platform/platform.dart'; +import 'package:shorebird_cli/src/archive/directory_archive.dart'; +import 'package:shorebird_cli/src/archive_analysis/archive_differ.dart'; +import 'package:shorebird_cli/src/archive_analysis/ios_archive_differ.dart'; +import 'package:shorebird_cli/src/artifact_builder.dart'; +import 'package:shorebird_cli/src/artifact_manager.dart'; +import 'package:shorebird_cli/src/code_push_client_wrapper.dart'; +import 'package:shorebird_cli/src/commands/patch_new/patch_new.dart'; +import 'package:shorebird_cli/src/doctor.dart'; +import 'package:shorebird_cli/src/executables/aot_tools.dart'; +import 'package:shorebird_cli/src/executables/xcodebuild.dart'; +import 'package:shorebird_cli/src/logger.dart'; +import 'package:shorebird_cli/src/patch_diff_checker.dart'; +import 'package:shorebird_cli/src/platform.dart'; +import 'package:shorebird_cli/src/platform/platform.dart'; +import 'package:shorebird_cli/src/release_type.dart'; +import 'package:shorebird_cli/src/shorebird_artifacts.dart'; +import 'package:shorebird_cli/src/shorebird_env.dart'; +import 'package:shorebird_cli/src/shorebird_flutter.dart'; +import 'package:shorebird_cli/src/shorebird_validator.dart'; +import 'package:shorebird_cli/src/third_party/flutter_tools/lib/flutter_tools.dart'; +import 'package:shorebird_cli/src/version.dart'; +import 'package:shorebird_code_push_client/shorebird_code_push_client.dart'; + +/// {@template ios_framework_patcher} +/// Functions to patch an iOS Framework release. +/// {@endtemplate} +class IosFrameworkPatcher extends Patcher { + /// {@macro ios_framework_patcher} + IosFrameworkPatcher({ + required super.argResults, + required super.flavor, + required super.target, + }); + + String get _buildDirectory => p.join( + shorebirdEnv.getShorebirdProjectRoot()!.path, + 'build', + ); + + String get _vmcodeOutputPath => p.join( + _buildDirectory, + 'out.vmcode', + ); + + @override + ArchiveDiffer get archiveDiffer => IosArchiveDiffer(); + + @override + String get primaryReleaseArtifactArch => 'xcframework'; + + @override + ReleaseType get releaseType => ReleaseType.iosFramework; + + @visibleForTesting + double? lastBuildLinkPercentage; + + @override + Future assertPreconditions() async { + try { + await shorebirdValidator.validatePreconditions( + checkUserIsAuthenticated: true, + checkShorebirdInitialized: true, + validators: doctor.iosCommandValidators, + supportedOperatingSystems: {Platform.macOS}, + ); + } on PreconditionFailedException catch (e) { + exit(e.exitCode.code); + } + } + + @override + Future assertArgsAreValid() async { + if (!argResults.wasParsed('release-version')) { + logger.err('Missing required argument: --release-version'); + exit(ExitCode.usage.code); + } + } + + @override + Future buildPatchArtifact() async { + final flutterVersionString = await shorebirdFlutter.getVersionAndRevision(); + final buildProgress = logger.progress( + 'Building patch with Flutter $flutterVersionString', + ); + try { + await artifactBuilder.buildIosFramework(); + } on ArtifactBuildException catch (error) { + buildProgress.fail(error.message); + exit(ExitCode.software.code); + } + try { + final newestDillFile = artifactManager.newestAppDill(); + await artifactBuilder.buildElfAotSnapshot( + appDillPath: newestDillFile.path, + outFilePath: p.join( + shorebirdEnv.getShorebirdProjectRoot()!.path, + 'build', + 'out.aot', + ), + ); + } catch (error) { + buildProgress.fail('$error'); + exit(ExitCode.software.code); + } + + buildProgress.complete(); + + return Directory( + p.join( + artifactManager.getAppXcframeworkDirectory().path, + ArtifactManager.appXcframeworkName, + ), + ).zipToTempFile(); + } + + @override + Future> createPatchArtifacts({ + required String appId, + required int releaseId, + }) async { + final releaseArtifact = await codePushClientWrapper.getReleaseArtifact( + appId: appId, + releaseId: releaseId, + arch: 'xcframework', + platform: ReleasePlatform.ios, + ); + + final downloadProgress = logger.progress('Downloading release artifact'); + final File releaseArtifactZipFile; + try { + releaseArtifactZipFile = await artifactManager.downloadFile( + Uri.parse(releaseArtifact.url), + ); + } catch (error) { + downloadProgress.fail('$error'); + exit(ExitCode.software.code); + } + downloadProgress.complete(); + + final unzipProgress = logger.progress('Extracting release artifact'); + final tempDir = Directory.systemTemp.createTempSync(); + await artifactManager.extractZip( + zipFile: releaseArtifactZipFile, + outputDirectory: tempDir, + ); + final releaseXcframeworkPath = tempDir.path; + + unzipProgress + .complete('Extracted release artifact to $releaseXcframeworkPath'); + final releaseArtifactFile = File( + p.join( + releaseXcframeworkPath, + 'ios-arm64', + 'App.framework', + 'App', + ), + ); + + final aotSnapshotFile = File( + p.join( + shorebirdEnv.getShorebirdProjectRoot()!.path, + 'build', + 'out.aot', + ), + ); + final useLinker = AotTools.usesLinker(shorebirdEnv.flutterRevision); + if (useLinker) { + await _runLinker( + aotSnapshot: aotSnapshotFile, + releaseArtifact: releaseArtifactFile, + ); + } + + final patchBuildFile = + useLinker ? File(_vmcodeOutputPath) : aotSnapshotFile; + final File patchFile; + if (await aotTools.isGeneratePatchDiffBaseSupported()) { + final patchBaseProgress = logger.progress('Generating patch diff base'); + final analyzeSnapshotPath = shorebirdArtifacts.getArtifactPath( + artifact: ShorebirdArtifact.analyzeSnapshot, + ); + + final File patchBaseFile; + try { + // If the aot_tools executable supports the dump_blobs command, we + // can generate a stable diff base and use that to create a patch. + patchBaseFile = await aotTools.generatePatchDiffBase( + analyzeSnapshotPath: analyzeSnapshotPath, + releaseSnapshot: releaseArtifactFile, + ); + patchBaseProgress.complete(); + } catch (error) { + patchBaseProgress.fail('$error'); + exit(ExitCode.software.code); + } + + patchFile = File( + await artifactManager.createDiff( + releaseArtifactPath: patchBaseFile.path, + patchArtifactPath: patchBuildFile.path, + ), + ); + } else { + patchFile = patchBuildFile; + } + + return { + Arch.arm64: PatchArtifactBundle( + arch: 'aarch64', + path: patchFile.path, + hash: sha256.convert(patchBuildFile.readAsBytesSync()).toString(), + size: patchFile.statSync().size, + ), + }; + } + + @override + Future extractReleaseVersionFromArtifact(File artifact) { + // Not implemented - release verison must be specified by the user. + throw UnimplementedError( + 'Release version must be specified using --release-version.', + ); + } + + @override + Future createPatchMetadata(DiffStatus diffStatus) async { + return CreatePatchMetadata( + releasePlatform: releaseType.releasePlatform, + usedIgnoreAssetChangesFlag: allowAssetDiffs, + hasAssetChanges: diffStatus.hasAssetChanges, + usedIgnoreNativeChangesFlag: allowNativeDiffs, + hasNativeChanges: diffStatus.hasNativeChanges, + linkPercentage: lastBuildLinkPercentage, + environment: BuildEnvironmentMetadata( + operatingSystem: platform.operatingSystem, + operatingSystemVersion: platform.operatingSystemVersion, + shorebirdVersion: packageVersion, + xcodeVersion: await xcodeBuild.version(), + ), + ); + } + + Future _runLinker({ + required File aotSnapshot, + required File releaseArtifact, + }) async { + if (!aotSnapshot.existsSync()) { + logger.err('Unable to find patch AOT file at ${aotSnapshot.path}'); + exit(ExitCode.software.code); + } + + final analyzeSnapshot = File( + shorebirdArtifacts.getArtifactPath( + artifact: ShorebirdArtifact.analyzeSnapshot, + ), + ); + + if (!analyzeSnapshot.existsSync()) { + logger.err('Unable to find analyze_snapshot at ${analyzeSnapshot.path}'); + exit(ExitCode.software.code); + } + + final genSnapshot = shorebirdArtifacts.getArtifactPath( + artifact: ShorebirdArtifact.genSnapshot, + ); + + final linkProgress = logger.progress('Linking AOT files'); + try { + lastBuildLinkPercentage = await aotTools.link( + base: releaseArtifact.path, + patch: aotSnapshot.path, + analyzeSnapshot: analyzeSnapshot.path, + genSnapshot: genSnapshot, + kernel: artifactManager.newestAppDill().path, + outputPath: _vmcodeOutputPath, + workingDirectory: _buildDirectory, + ); + } catch (error) { + linkProgress.fail('Failed to link AOT files: $error'); + exit(ExitCode.software.code); + } + + linkProgress.complete(); + } +} diff --git a/packages/shorebird_cli/lib/src/commands/patch_new/patch_new.dart b/packages/shorebird_cli/lib/src/commands/patch_new/patch_new.dart index 1f46b88f..41481282 100644 --- a/packages/shorebird_cli/lib/src/commands/patch_new/patch_new.dart +++ b/packages/shorebird_cli/lib/src/commands/patch_new/patch_new.dart @@ -1,4 +1,5 @@ export 'aar_patcher.dart'; export 'android_patcher.dart'; +export 'ios_framework_patcher.dart'; export 'patch_new_command.dart'; export 'patcher.dart'; diff --git a/packages/shorebird_cli/lib/src/commands/patch_new/patch_new_command.dart b/packages/shorebird_cli/lib/src/commands/patch_new/patch_new_command.dart index f92cc60b..9376accd 100644 --- a/packages/shorebird_cli/lib/src/commands/patch_new/patch_new_command.dart +++ b/packages/shorebird_cli/lib/src/commands/patch_new/patch_new_command.dart @@ -3,6 +3,7 @@ import 'package:meta/meta.dart'; import 'package:scoped/scoped.dart'; import 'package:shorebird_cli/src/archive_analysis/archive_differ.dart'; import 'package:shorebird_cli/src/artifact_manager.dart'; +import 'package:shorebird_cli/src/cache.dart'; import 'package:shorebird_cli/src/code_push_client_wrapper.dart'; import 'package:shorebird_cli/src/command.dart'; import 'package:shorebird_cli/src/commands/patch_new/patch_new.dart'; @@ -137,7 +138,11 @@ NOTE: this is ${styleBold.wrap('not')} recommended. Asset changes cannot be incl case ReleaseType.ios: throw UnimplementedError(); case ReleaseType.iosFramework: - throw UnimplementedError(); + return IosFrameworkPatcher( + argResults: results, + flavor: flavor, + target: target, + ); case ReleaseType.aar: return AarPatcher( argResults: results, @@ -157,6 +162,8 @@ NOTE: this is ${styleBold.wrap('not')} recommended. Asset changes cannot be incl await patcher.assertPreconditions(); await patcher.assertArgsAreValid(); + await cache.updateAll(); + final app = await codePushClientWrapper.getApp(appId: appId); File? patchArtifact; @@ -186,6 +193,8 @@ NOTE: this is ${styleBold.wrap('not')} recommended. Asset changes cannot be incl return await runScoped( () async { + await cache.updateAll(); + // Don't built the patch artifact twice with the same Flutter revision. if (lastBuiltFlutterRevision != release.flutterRevision) { patchArtifact = await patcher.buildPatchArtifact(); diff --git a/packages/shorebird_cli/lib/src/commands/release_new/android_releaser.dart b/packages/shorebird_cli/lib/src/commands/release_new/android_releaser.dart index 80142615..b32321b4 100644 --- a/packages/shorebird_cli/lib/src/commands/release_new/android_releaser.dart +++ b/packages/shorebird_cli/lib/src/commands/release_new/android_releaser.dart @@ -1,8 +1,8 @@ import 'package:mason_logger/mason_logger.dart'; import 'package:shorebird_cli/src/artifact_builder.dart'; import 'package:shorebird_cli/src/code_push_client_wrapper.dart'; -import 'package:shorebird_cli/src/commands/release_new/releaser.dart'; import 'package:shorebird_cli/src/commands/release_new/release_new.dart'; +import 'package:shorebird_cli/src/commands/release_new/releaser.dart'; import 'package:shorebird_cli/src/doctor.dart'; import 'package:shorebird_cli/src/logger.dart'; import 'package:shorebird_cli/src/platform.dart'; diff --git a/packages/shorebird_cli/lib/src/commands/release_new/ios_framework_releaser.dart b/packages/shorebird_cli/lib/src/commands/release_new/ios_framework_releaser.dart index 6bac65fa..e3b11a15 100644 --- a/packages/shorebird_cli/lib/src/commands/release_new/ios_framework_releaser.dart +++ b/packages/shorebird_cli/lib/src/commands/release_new/ios_framework_releaser.dart @@ -5,12 +5,12 @@ import 'package:platform/platform.dart'; import 'package:shorebird_cli/src/artifact_builder.dart'; import 'package:shorebird_cli/src/artifact_manager.dart'; import 'package:shorebird_cli/src/code_push_client_wrapper.dart'; -import 'package:shorebird_cli/src/release_type.dart'; import 'package:shorebird_cli/src/commands/release_new/releaser.dart'; import 'package:shorebird_cli/src/doctor.dart'; import 'package:shorebird_cli/src/executables/xcodebuild.dart'; import 'package:shorebird_cli/src/logger.dart'; import 'package:shorebird_cli/src/platform.dart'; +import 'package:shorebird_cli/src/release_type.dart'; import 'package:shorebird_cli/src/shorebird_env.dart'; import 'package:shorebird_cli/src/shorebird_flutter.dart'; import 'package:shorebird_cli/src/shorebird_validator.dart'; @@ -92,6 +92,20 @@ class IosFrameworkReleaser extends Releaser { targetLibraryDirectory.path, ); + // Rename Flutter.xcframework to ShorebirdFlutter.xcframework to avoid + // Xcode warning users about the .xcframework signature changing. + Directory( + p.join( + targetLibraryDirectory.path, + 'Flutter.xcframework', + ), + ).renameSync( + p.join( + targetLibraryDirectory.path, + 'ShorebirdFlutter.xcframework', + ), + ); + return targetLibraryDirectory; } diff --git a/packages/shorebird_cli/lib/src/commands/release_new/release_new_command.dart b/packages/shorebird_cli/lib/src/commands/release_new/release_new_command.dart index 5fc36a42..41a0c36c 100644 --- a/packages/shorebird_cli/lib/src/commands/release_new/release_new_command.dart +++ b/packages/shorebird_cli/lib/src/commands/release_new/release_new_command.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:mason_logger/mason_logger.dart'; import 'package:meta/meta.dart'; import 'package:scoped/scoped.dart'; +import 'package:shorebird_cli/src/cache.dart'; import 'package:shorebird_cli/src/code_push_client_wrapper.dart'; import 'package:shorebird_cli/src/command.dart'; import 'package:shorebird_cli/src/commands/release_new/release_new.dart'; @@ -178,6 +179,8 @@ of the iOS app that is using this module.''', await releaser.assertPreconditions(); await releaser.assertArgsAreValid(); + await cache.updateAll(); + // This command handles logging, we don't need to provide our own // progress, error logs, etc. final app = await codePushClientWrapper.getApp(appId: appId); @@ -193,6 +196,8 @@ of the iOS app that is using this module.''', ); return await runScoped( () async { + await cache.updateAll(); + final releaseArtifact = await releaser.buildReleaseArtifacts(); final releaseVersion = await releaser.getReleaseVersion( releaseArtifactRoot: releaseArtifact, diff --git a/packages/shorebird_cli/test/src/artifact_builder_test.dart b/packages/shorebird_cli/test/src/artifact_builder_test.dart index a0ed3248..8c5d14c1 100644 --- a/packages/shorebird_cli/test/src/artifact_builder_test.dart +++ b/packages/shorebird_cli/test/src/artifact_builder_test.dart @@ -9,6 +9,7 @@ import 'package:shorebird_cli/src/logger.dart'; import 'package:shorebird_cli/src/os/operating_system_interface.dart'; import 'package:shorebird_cli/src/platform/platform.dart'; import 'package:shorebird_cli/src/shorebird_android_artifacts.dart'; +import 'package:shorebird_cli/src/shorebird_artifacts.dart'; import 'package:shorebird_cli/src/shorebird_env.dart'; import 'package:shorebird_cli/src/shorebird_process.dart'; import 'package:test/test.dart'; @@ -22,6 +23,7 @@ void main() { late ShorebirdLogger logger; late OperatingSystemInterface operatingSystemInterface; late ShorebirdAndroidArtifacts shorebirdAndroidArtifacts; + late ShorebirdArtifacts shorebirdArtifacts; late ShorebirdEnv shorebirdEnv; late ShorebirdProcess shorebirdProcess; late ShorebirdProcessResult buildProcessResult; @@ -36,6 +38,7 @@ void main() { loggerRef.overrideWith(() => logger), osInterfaceRef.overrideWith(() => operatingSystemInterface), processRef.overrideWith(() => shorebirdProcess), + shorebirdArtifactsRef.overrideWith(() => shorebirdArtifacts), shorebirdEnvRef.overrideWith(() => shorebirdEnv), shorebirdAndroidArtifactsRef .overrideWith(() => shorebirdAndroidArtifacts), @@ -55,6 +58,7 @@ void main() { operatingSystemInterface = MockOperatingSystemInterface(); pubGetProcessResult = MockProcessResult(); shorebirdAndroidArtifacts = MockShorebirdAndroidArtifacts(); + shorebirdArtifacts = MockShorebirdArtifacts(); shorebirdEnv = MockShorebirdEnv(); shorebirdProcess = MockShorebirdProcess(); @@ -772,6 +776,48 @@ Failed to build: }); }); }); + + group('buildElfAotSnapshot', () { + setUp(() { + when( + () => shorebirdArtifacts.getArtifactPath( + artifact: ShorebirdArtifact.genSnapshot, + ), + ).thenReturn('gen_snapshot'); + }); + + group('when build fails', () { + setUp(() { + when(() => buildProcessResult.exitCode) + .thenReturn(ExitCode.software.code); + }); + + test('throws ArtifactBuildException', () { + expect( + () => runWithOverrides( + () => builder.buildElfAotSnapshot( + appDillPath: 'asdf', + outFilePath: 'asdf', + ), + ), + throwsA(isA()), + ); + }); + }); + + group('when build succeeds', () { + test('returns outFile', () async { + final outFile = await runWithOverrides( + () => builder.buildElfAotSnapshot( + appDillPath: '/app/dill/path', + outFilePath: '/path/to/out', + ), + ); + + expect(outFile.path, '/path/to/out'); + }); + }); + }); }, testOn: 'mac-os', ); diff --git a/packages/shorebird_cli/test/src/artifact_manager_test.dart b/packages/shorebird_cli/test/src/artifact_manager_test.dart index ed39e46a..2216e2ee 100644 --- a/packages/shorebird_cli/test/src/artifact_manager_test.dart +++ b/packages/shorebird_cli/test/src/artifact_manager_test.dart @@ -592,5 +592,30 @@ void main() { ); }); }); + + group('newestAppDill', () { + late File appDill2; + + setUp(() { + final tempDir = Directory.systemTemp.createTempSync(); + when(() => shorebirdEnv.getShorebirdProjectRoot()).thenReturn(tempDir); + final flutterBuildDir = Directory( + p.join(tempDir.path, '.dart_tool', 'flutter_build'), + )..createSync(recursive: true); + File(p.join(flutterBuildDir.path, 'app1', 'app.dill')) + ..createSync(recursive: true) + ..setLastModified(DateTime.now().subtract(const Duration(days: 1))); + appDill2 = File(p.join(flutterBuildDir.path, 'app2', 'app.dill')) + ..createSync(recursive: true) + ..setLastModified(DateTime.now()); + }); + + test('selects the most recently edited .app.dill file', () { + final result = runWithOverrides(artifactManager.newestAppDill); + + expect(result, isNotNull); + expect(result.path, equals(appDill2.path)); + }); + }); }); } diff --git a/packages/shorebird_cli/test/src/commands/patch_new/ios_framework_patcher_test.dart b/packages/shorebird_cli/test/src/commands/patch_new/ios_framework_patcher_test.dart new file mode 100644 index 00000000..8a28997c --- /dev/null +++ b/packages/shorebird_cli/test/src/commands/patch_new/ios_framework_patcher_test.dart @@ -0,0 +1,816 @@ +import 'package:args/args.dart'; +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/archive_analysis/archive_analysis.dart'; +import 'package:shorebird_cli/src/artifact_builder.dart'; +import 'package:shorebird_cli/src/artifact_manager.dart'; +import 'package:shorebird_cli/src/code_push_client_wrapper.dart'; +import 'package:shorebird_cli/src/commands/patch_new/patch_new.dart'; +import 'package:shorebird_cli/src/doctor.dart'; +import 'package:shorebird_cli/src/engine_config.dart'; +import 'package:shorebird_cli/src/executables/aot_tools.dart'; +import 'package:shorebird_cli/src/executables/xcodebuild.dart'; +import 'package:shorebird_cli/src/logger.dart'; +import 'package:shorebird_cli/src/os/operating_system_interface.dart'; +import 'package:shorebird_cli/src/patch_diff_checker.dart'; +import 'package:shorebird_cli/src/platform.dart'; +import 'package:shorebird_cli/src/platform/platform.dart'; +import 'package:shorebird_cli/src/release_type.dart'; +import 'package:shorebird_cli/src/shorebird_artifacts.dart'; +import 'package:shorebird_cli/src/shorebird_env.dart'; +import 'package:shorebird_cli/src/shorebird_flutter.dart'; +import 'package:shorebird_cli/src/shorebird_process.dart'; +import 'package:shorebird_cli/src/shorebird_validator.dart'; +import 'package:shorebird_cli/src/third_party/flutter_tools/lib/flutter_tools.dart'; +import 'package:shorebird_cli/src/validators/validators.dart'; +import 'package:shorebird_cli/src/version.dart'; +import 'package:shorebird_code_push_client/shorebird_code_push_client.dart'; +import 'package:test/test.dart'; + +import '../../matchers.dart'; +import '../../mocks.dart'; + +void main() { + group( + IosFrameworkPatcher, + () { + late AotTools aotTools; + late ArgResults argResults; + late ArtifactBuilder artifactBuilder; + late ArtifactManager artifactManager; + late CodePushClientWrapper codePushClientWrapper; + late Doctor doctor; + late EngineConfig engineConfig; + late Directory flutterDirectory; + late Directory projectRoot; + late ShorebirdLogger logger; + late OperatingSystemInterface operatingSystemInterface; + late Platform platform; + late Progress progress; + late ShorebirdArtifacts shorebirdArtifacts; + late ShorebirdFlutterValidator flutterValidator; + late ShorebirdProcess shorebirdProcess; + late ShorebirdEnv shorebirdEnv; + late ShorebirdFlutter shorebirdFlutter; + late ShorebirdValidator shorebirdValidator; + late XcodeBuild xcodeBuild; + late IosFrameworkPatcher patcher; + + R runWithOverrides(R Function() body) { + return runScoped( + body, + values: { + aotToolsRef.overrideWith(() => aotTools), + artifactBuilderRef.overrideWith(() => artifactBuilder), + artifactManagerRef.overrideWith(() => artifactManager), + codePushClientWrapperRef.overrideWith(() => codePushClientWrapper), + doctorRef.overrideWith(() => doctor), + engineConfigRef.overrideWith(() => engineConfig), + loggerRef.overrideWith(() => logger), + osInterfaceRef.overrideWith(() => operatingSystemInterface), + platformRef.overrideWith(() => platform), + processRef.overrideWith(() => shorebirdProcess), + shorebirdArtifactsRef.overrideWith(() => shorebirdArtifacts), + shorebirdEnvRef.overrideWith(() => shorebirdEnv), + shorebirdFlutterRef.overrideWith(() => shorebirdFlutter), + shorebirdValidatorRef.overrideWith(() => shorebirdValidator), + xcodeBuildRef.overrideWith(() => xcodeBuild), + }, + ); + } + + setUpAll(() { + registerFallbackValue(Directory('')); + registerFallbackValue(File('')); + registerFallbackValue(ReleasePlatform.ios); + registerFallbackValue(Uri.parse('https://example.com')); + setExitFunctionForTests(); + }); + + tearDownAll(restoreExitFunction); + + setUp(() { + aotTools = MockAotTools(); + argResults = MockArgResults(); + artifactBuilder = MockArtifactBuilder(); + artifactManager = MockArtifactManager(); + codePushClientWrapper = MockCodePushClientWrapper(); + doctor = MockDoctor(); + engineConfig = MockEngineConfig(); + operatingSystemInterface = MockOperatingSystemInterface(); + platform = MockPlatform(); + progress = MockProgress(); + projectRoot = Directory.systemTemp.createTempSync(); + logger = MockShorebirdLogger(); + shorebirdArtifacts = MockShorebirdArtifacts(); + shorebirdProcess = MockShorebirdProcess(); + shorebirdEnv = MockShorebirdEnv(); + flutterValidator = MockShorebirdFlutterValidator(); + shorebirdFlutter = MockShorebirdFlutter(); + shorebirdValidator = MockShorebirdValidator(); + xcodeBuild = MockXcodeBuild(); + + when(() => argResults['build-number']).thenReturn('1.0'); + + when(() => logger.progress(any())).thenReturn(progress); + + when( + () => shorebirdEnv.getShorebirdProjectRoot(), + ).thenReturn(projectRoot); + + patcher = IosFrameworkPatcher( + argResults: argResults, + flavor: null, + target: null, + ); + }); + + group('archiveDiffer', () { + test('is an IosArchiveDiffer', () { + expect(patcher.archiveDiffer, isA()); + }); + }); + + group('primaryReleaseArtifactArch', () { + test('is "xcframework"', () { + expect(patcher.primaryReleaseArtifactArch, 'xcframework'); + }); + }); + + group('releaseType', () { + test('is ReleaseType.iosFramework', () { + expect(patcher.releaseType, ReleaseType.iosFramework); + }); + }); + + group('assertPreconditions', () { + setUp(() { + when(() => doctor.iosCommandValidators) + .thenReturn([flutterValidator]); + }); + + group('when validation succeeds', () { + setUp(() { + when( + () => shorebirdValidator.validatePreconditions( + checkUserIsAuthenticated: + any(named: 'checkUserIsAuthenticated'), + checkShorebirdInitialized: + any(named: 'checkShorebirdInitialized'), + validators: any(named: 'validators'), + supportedOperatingSystems: + any(named: 'supportedOperatingSystems'), + ), + ).thenAnswer((_) async {}); + }); + + test('returns normally', () async { + await expectLater( + () => runWithOverrides(patcher.assertPreconditions), + returnsNormally, + ); + }); + }); + + group('when validation fails', () { + setUp(() { + final exception = ValidationFailedException(); + when( + () => shorebirdValidator.validatePreconditions( + checkUserIsAuthenticated: + any(named: 'checkUserIsAuthenticated'), + checkShorebirdInitialized: + any(named: 'checkShorebirdInitialized'), + validators: any(named: 'validators'), + ), + ).thenThrow(exception); + }); + + test('exits with code 70', () async { + final exception = ValidationFailedException(); + when( + () => shorebirdValidator.validatePreconditions( + checkUserIsAuthenticated: + any(named: 'checkUserIsAuthenticated'), + checkShorebirdInitialized: + any(named: 'checkShorebirdInitialized'), + validators: any(named: 'validators'), + supportedOperatingSystems: + any(named: 'supportedOperatingSystems'), + ), + ).thenThrow(exception); + await expectLater( + () => runWithOverrides(patcher.assertPreconditions), + exitsWithCode(exception.exitCode), + ); + verify( + () => shorebirdValidator.validatePreconditions( + checkUserIsAuthenticated: true, + checkShorebirdInitialized: true, + validators: [flutterValidator], + supportedOperatingSystems: {Platform.macOS}, + ), + ).called(1); + }); + }); + }); + + group('assertArgsAreValid', () { + group('when release-version is not provided', () { + setUp(() { + when(() => argResults.wasParsed('release-version')) + .thenReturn(false); + }); + + test('exits with code 64', () async { + await expectLater( + () => runWithOverrides(patcher.assertArgsAreValid), + exitsWithCode(ExitCode.usage), + ); + }); + }); + + group('when arguments are valid', () { + setUp(() { + when(() => argResults.wasParsed('release-version')) + .thenReturn(true); + }); + + test('returns normally', () { + expect( + () => runWithOverrides(patcher.assertArgsAreValid), + returnsNormally, + ); + }); + }); + }); + + group('buildPatchArtifact', () { + const flutterVersionAndRevision = '3.10.6 (83305b5088)'; + + setUp(() { + when( + () => shorebirdFlutter.getVersionAndRevision(), + ).thenAnswer((_) async => flutterVersionAndRevision); + }); + + group('when build fails', () { + setUp(() { + when(() => artifactBuilder.buildIosFramework()).thenThrow( + ArtifactBuildException('Build failed'), + ); + }); + + test('exits with code 70', () async { + await expectLater( + () => runWithOverrides(patcher.buildPatchArtifact), + exitsWithCode(ExitCode.software), + ); + + verify(() => progress.fail('Build failed')); + }); + }); + + group('when elf aot snapshot build fails', () { + setUp(() { + when(() => artifactBuilder.buildIosFramework()).thenAnswer( + (_) async {}, + ); + when(() => artifactManager.newestAppDill()).thenReturn(File('')); + when( + () => artifactBuilder.buildElfAotSnapshot( + appDillPath: any(named: 'appDillPath'), + outFilePath: any(named: 'outFilePath'), + ), + ).thenThrow(const FileSystemException('error')); + }); + + test('logs error and exits with code 70', () async { + await expectLater( + () => runWithOverrides(patcher.buildPatchArtifact), + exitsWithCode(ExitCode.software), + ); + + verify( + () => progress.fail("FileSystemException: error, path = ''"), + ); + }); + }); + + group('when build succeeds', () { + setUp(() { + when(() => artifactBuilder.buildIosFramework()).thenAnswer( + (_) async {}, + ); + when(() => artifactManager.newestAppDill()).thenReturn(File('')); + when( + () => artifactBuilder.buildElfAotSnapshot( + appDillPath: any(named: 'appDillPath'), + outFilePath: any(named: 'outFilePath'), + ), + ).thenAnswer( + (invocation) async => + File(invocation.namedArguments[#outFilePath] as String) + ..createSync(recursive: true), + ); + + Directory( + p.join(projectRoot.path, ArtifactManager.appXcframeworkName), + ).createSync(recursive: true); + when(() => artifactManager.getAppXcframeworkDirectory()) + .thenReturn(projectRoot); + }); + + test('returns zipped xcframework', () async { + final artifact = await runWithOverrides(patcher.buildPatchArtifact); + expect(p.basename(artifact.path), equals('App.xcframework.zip')); + }); + }); + }); + + group('createPatchArtifacts', () { + const postLinkerFlutterRevision = + 'deadbeefdeadbeefdeadbeefdeadbeefdeadbeef'; + const preLinkerFlutterRevision = + '83305b5088e6fe327fb3334a73ff190828d85713'; + const appId = 'appId'; + const arch = 'aarch64'; + const releaseId = 1; + const linkFileName = 'out.vmcode'; + const elfAotSnapshotFileName = 'out.aot'; + const releaseArtifact = ReleaseArtifact( + id: 0, + releaseId: releaseId, + arch: arch, + platform: ReleasePlatform.android, + hash: '#', + size: 42, + url: 'https://example.com', + ); + + void setUpProjectRootArtifacts() { + // Create a second app.dill for coverage of newestAppDill file. + File( + p.join( + projectRoot.path, + '.dart_tool', + 'flutter_build', + 'subdir', + 'app.dill', + ), + ).createSync(recursive: true); + File( + p.join(projectRoot.path, '.dart_tool', 'flutter_build', 'app.dill'), + ).createSync(recursive: true); + File(p.join(projectRoot.path, 'build', elfAotSnapshotFileName)) + .createSync( + recursive: true, + ); + Directory( + p.join( + projectRoot.path, + 'build', + 'ios', + 'framework', + 'Release', + 'App.xcframework', + ), + ).createSync( + recursive: true, + ); + File( + p.join(projectRoot.path, 'build', linkFileName), + ).createSync(recursive: true); + } + + setUp(() { + when( + () => codePushClientWrapper.getReleaseArtifact( + appId: any(named: 'appId'), + releaseId: any(named: 'releaseId'), + arch: any(named: 'arch'), + platform: any(named: 'platform'), + ), + ).thenAnswer((_) async => releaseArtifact); + when(() => artifactManager.downloadFile(any())).thenAnswer((_) async { + final tempDirectory = Directory.systemTemp.createTempSync(); + final file = File(p.join(tempDirectory.path, 'libapp.so')) + ..createSync(); + return file; + }); + when( + () => artifactManager.extractZip( + zipFile: any(named: 'zipFile'), + outputDirectory: any(named: 'outputDirectory'), + ), + ).thenAnswer((invocation) async { + final zipFile = invocation.namedArguments[#zipFile] as File; + final outDir = + invocation.namedArguments[#outputDirectory] as Directory; + File(p.join(outDir.path, '${p.basename(zipFile.path)}.zip')) + .createSync(); + }); + when(() => engineConfig.localEngine).thenReturn(null); + }); + + group('when release artifact download fails', () { + setUp(() { + when( + () => artifactManager.downloadFile(any()), + ).thenThrow(Exception('Failed to download release artifact')); + }); + + test('logs error and exits with code 70', () async { + await expectLater( + () => runWithOverrides( + () => patcher.createPatchArtifacts( + appId: appId, + releaseId: releaseId, + ), + ), + exitsWithCode(ExitCode.software), + ); + }); + }); + + group('when uses linker', () { + const linkPercentage = 50.0; + late File analyzeSnapshotFile; + late File genSnapshotFile; + + setUp(() { + final shorebirdRoot = Directory.systemTemp.createTempSync(); + flutterDirectory = Directory( + p.join(shorebirdRoot.path, 'bin', 'cache', 'flutter'), + ); + genSnapshotFile = File( + p.join( + flutterDirectory.path, + 'bin', + 'cache', + 'artifacts', + 'engine', + 'ios-release', + 'gen_snapshot_arm64', + ), + ); + analyzeSnapshotFile = File( + p.join( + flutterDirectory.path, + 'bin', + 'cache', + 'artifacts', + 'engine', + 'ios-release', + 'analyze_snapshot_arm64', + ), + )..createSync(recursive: true); + + when( + () => aotTools.link( + base: any(named: 'base'), + patch: any(named: 'patch'), + analyzeSnapshot: any(named: 'analyzeSnapshot'), + genSnapshot: any(named: 'genSnapshot'), + kernel: any(named: 'kernel'), + outputPath: any(named: 'outputPath'), + workingDirectory: any(named: 'workingDirectory'), + ), + ).thenAnswer((_) async => linkPercentage); + when(() => artifactManager.newestAppDill()).thenReturn(File('')); + when(() => shorebirdEnv.flutterRevision) + .thenReturn(postLinkerFlutterRevision); + when( + () => shorebirdArtifacts.getArtifactPath( + artifact: ShorebirdArtifact.analyzeSnapshot, + ), + ).thenReturn(analyzeSnapshotFile.path); + when( + () => shorebirdArtifacts.getArtifactPath( + artifact: ShorebirdArtifact.genSnapshot, + ), + ).thenReturn(genSnapshotFile.path); + }); + + group('when linking fails', () { + group('when aot snapshot does not exist', () { + test('logs error and exits with code 70', () async { + await expectLater( + () => runWithOverrides( + () => patcher.createPatchArtifacts( + appId: appId, + releaseId: releaseId, + ), + ), + exitsWithCode(ExitCode.software), + ); + + verify( + () => logger.err( + any(that: startsWith('Unable to find patch AOT file at')), + ), + ).called(1); + }); + }); + + group('when analyzeSnapshot binary does not exist', () { + setUp(() { + when( + () => shorebirdArtifacts.getArtifactPath( + artifact: ShorebirdArtifact.analyzeSnapshot), + ).thenReturn(''); + setUpProjectRootArtifacts(); + }); + + test('logs error and exits with code 70', () async { + await expectLater( + () => runWithOverrides( + () => patcher.createPatchArtifacts( + appId: appId, + releaseId: releaseId, + ), + ), + exitsWithCode(ExitCode.software), + ); + + verify( + () => logger.err('Unable to find analyze_snapshot at '), + ).called(1); + }); + }); + + group('when call to aotTools.link fails', () { + setUp(() { + when( + () => aotTools.link( + base: any(named: 'base'), + patch: any(named: 'patch'), + analyzeSnapshot: any(named: 'analyzeSnapshot'), + genSnapshot: any(named: 'genSnapshot'), + kernel: any(named: 'kernel'), + outputPath: any(named: 'outputPath'), + workingDirectory: any(named: 'workingDirectory'), + ), + ).thenThrow(Exception('oops')); + + setUpProjectRootArtifacts(); + }); + + test('logs error and exits with code 70', () async { + await expectLater( + () => runWithOverrides( + () => patcher.createPatchArtifacts( + appId: appId, + releaseId: releaseId, + ), + ), + exitsWithCode(ExitCode.software), + ); + + verify( + () => progress.fail( + 'Failed to link AOT files: Exception: oops', + ), + ).called(1); + }); + }); + }); + + group('when generate patch diff base is supported', () { + setUp(() { + when(() => aotTools.isGeneratePatchDiffBaseSupported()) + .thenAnswer((_) async => true); + when( + () => aotTools.generatePatchDiffBase( + analyzeSnapshotPath: any(named: 'analyzeSnapshotPath'), + releaseSnapshot: any(named: 'releaseSnapshot'), + ), + ).thenAnswer((_) async => File('')); + }); + + group('when we fail to generate patch diff base', () { + setUp(() { + when( + () => aotTools.generatePatchDiffBase( + analyzeSnapshotPath: any(named: 'analyzeSnapshotPath'), + releaseSnapshot: any(named: 'releaseSnapshot'), + ), + ).thenThrow(Exception('oops')); + + setUpProjectRootArtifacts(); + }); + + test('logs error and exits with code 70', () async { + await expectLater( + () => runWithOverrides( + () => patcher.createPatchArtifacts( + appId: appId, + releaseId: releaseId, + ), + ), + exitsWithCode(ExitCode.software), + ); + + verify(() => progress.fail('Exception: oops')).called(1); + }); + }); + + group('when linking and patch diff generation succeeds', () { + const diffPath = 'path/to/diff'; + + setUp(() { + when( + () => artifactManager.createDiff( + releaseArtifactPath: any(named: 'releaseArtifactPath'), + patchArtifactPath: any(named: 'patchArtifactPath'), + ), + ).thenAnswer((_) async => diffPath); + setUpProjectRootArtifacts(); + }); + + test('returns linked patch artifact in patch bundle', () async { + final patchBundle = await runWithOverrides( + () => patcher.createPatchArtifacts( + appId: appId, + releaseId: releaseId, + ), + ); + + expect(patchBundle, hasLength(1)); + expect( + patchBundle[Arch.arm64], + isA() + .having((b) => b.path, 'path', endsWith(diffPath)), + ); + }); + }); + }); + + group('when generate patch diff base is not supported', () { + setUp(() { + when(aotTools.isGeneratePatchDiffBaseSupported) + .thenAnswer((_) async => false); + setUpProjectRootArtifacts(); + }); + + test('returns vmcode file as patch file', () async { + final patchBundle = await runWithOverrides( + () => patcher.createPatchArtifacts( + appId: appId, + releaseId: releaseId, + ), + ); + + expect(patchBundle, hasLength(1)); + expect( + patchBundle[Arch.arm64], + isA() + .having((b) => b.path, 'path', endsWith('out.vmcode')), + ); + }); + }); + }); + + group('when does not use linker', () { + setUp(() { + when(() => shorebirdEnv.flutterRevision) + .thenReturn(preLinkerFlutterRevision); + when(() => aotTools.isGeneratePatchDiffBaseSupported()) + .thenAnswer((_) async => false); + + setUpProjectRootArtifacts(); + }); + + test('returns base patch artifact in patch bundle', () async { + final patchArtifacts = await runWithOverrides( + () => patcher.createPatchArtifacts( + appId: appId, + releaseId: releaseId, + ), + ); + + expect(patchArtifacts, hasLength(1)); + verifyNever( + () => aotTools.link( + base: any(named: 'base'), + patch: any(named: 'patch'), + analyzeSnapshot: any(named: 'analyzeSnapshot'), + genSnapshot: any(named: 'genSnapshot'), + kernel: any(named: 'kernel'), + outputPath: any(named: 'outputPath'), + ), + ); + }); + }); + }); + + group('extractReleaseVersionFromArtifact', () { + test('throws UnimplementedError', () { + expect( + () => patcher.extractReleaseVersionFromArtifact(File('')), + throwsUnimplementedError, + ); + }); + }); + + group('createPatchMetadata', () { + const allowAssetDiffs = false; + const allowNativeDiffs = true; + const operatingSystem = 'Mac OS X'; + const operatingSystemVersion = '10.15.7'; + const xcodeVersion = '11'; + + setUp(() { + when(() => argResults['allow-asset-diffs']) + .thenReturn(allowAssetDiffs); + when( + () => argResults['allow-native-diffs'], + ).thenReturn(allowNativeDiffs); + when(() => platform.operatingSystem).thenReturn(operatingSystem); + when( + () => platform.operatingSystemVersion, + ).thenReturn(operatingSystemVersion); + + when(() => xcodeBuild.version()) + .thenAnswer((_) async => xcodeVersion); + }); + + group('when linker is not enabled', () { + test('returns correct metadata', () async { + final diffStatus = DiffStatus( + hasAssetChanges: false, + hasNativeChanges: false, + ); + + final metadata = await runWithOverrides( + () => patcher.createPatchMetadata(diffStatus), + ); + + expect( + metadata, + equals( + CreatePatchMetadata( + releasePlatform: ReleasePlatform.ios, + usedIgnoreAssetChangesFlag: allowAssetDiffs, + hasAssetChanges: diffStatus.hasAssetChanges, + usedIgnoreNativeChangesFlag: allowNativeDiffs, + hasNativeChanges: diffStatus.hasNativeChanges, + linkPercentage: null, + environment: const BuildEnvironmentMetadata( + operatingSystem: operatingSystem, + operatingSystemVersion: operatingSystemVersion, + shorebirdVersion: packageVersion, + xcodeVersion: xcodeVersion, + ), + ), + ), + ); + }); + }); + + group('when linker is enabled', () { + const linkPercentage = 100.0; + + setUp(() { + patcher.lastBuildLinkPercentage = linkPercentage; + }); + + test('returns correct metadata', () async { + final diffStatus = DiffStatus( + hasAssetChanges: false, + hasNativeChanges: false, + ); + + final metadata = await runWithOverrides( + () => patcher.createPatchMetadata(diffStatus), + ); + + expect( + metadata, + equals( + CreatePatchMetadata( + releasePlatform: ReleasePlatform.ios, + usedIgnoreAssetChangesFlag: allowAssetDiffs, + hasAssetChanges: diffStatus.hasAssetChanges, + usedIgnoreNativeChangesFlag: allowNativeDiffs, + hasNativeChanges: diffStatus.hasNativeChanges, + linkPercentage: linkPercentage, + environment: const BuildEnvironmentMetadata( + operatingSystem: operatingSystem, + operatingSystemVersion: operatingSystemVersion, + shorebirdVersion: packageVersion, + xcodeVersion: xcodeVersion, + ), + ), + ), + ); + }); + }); + }); + }, + testOn: 'mac-os', + ); +} diff --git a/packages/shorebird_cli/test/src/commands/patch_new/patch_new_command_test.dart b/packages/shorebird_cli/test/src/commands/patch_new/patch_new_command_test.dart index ec7def43..721e6eef 100644 --- a/packages/shorebird_cli/test/src/commands/patch_new/patch_new_command_test.dart +++ b/packages/shorebird_cli/test/src/commands/patch_new/patch_new_command_test.dart @@ -6,6 +6,7 @@ import 'package:shorebird_cli/src/archive_analysis/archive_analysis.dart'; import 'package:shorebird_cli/src/archive_analysis/archive_differ.dart'; import 'package:shorebird_cli/src/artifact_builder.dart'; import 'package:shorebird_cli/src/artifact_manager.dart'; +import 'package:shorebird_cli/src/cache.dart'; import 'package:shorebird_cli/src/code_push_client_wrapper.dart'; import 'package:shorebird_cli/src/commands/patch_new/patch_new.dart'; import 'package:shorebird_cli/src/config/config.dart'; @@ -75,6 +76,7 @@ void main() { late ArgResults argResults; late ArtifactBuilder artifactBuilder; late ArtifactManager artifactManager; + late Cache cache; late CodePushClientWrapper codePushClientWrapper; late ShorebirdLogger logger; late PatchDiffChecker patchDiffChecker; @@ -91,6 +93,7 @@ void main() { values: { artifactBuilderRef.overrideWith(() => artifactBuilder), artifactManagerRef.overrideWith(() => artifactManager), + cacheRef.overrideWith(() => cache), codePushClientWrapperRef.overrideWith(() => codePushClientWrapper), loggerRef.overrideWith(() => logger), patchDiffCheckerRef.overrideWith(() => patchDiffChecker), @@ -119,6 +122,7 @@ void main() { argResults = MockArgResults(); artifactBuilder = MockArtifactBuilder(); artifactManager = MockArtifactManager(); + cache = MockCache(); codePushClientWrapper = MockCodePushClientWrapper(); logger = MockShorebirdLogger(); progress = MockProgress(); @@ -136,6 +140,8 @@ void main() { () => artifactManager.downloadFile(any()), ).thenAnswer((_) async => File('')); + when(() => cache.updateAll()).thenAnswer((_) async => {}); + when(() => codePushClientWrapper.getApp(appId: any(named: 'appId'))) .thenAnswer((_) async => appMetadata); when( @@ -278,8 +284,8 @@ void main() { throwsA(isA()), ); expect( - () => command.getPatcher(ReleaseType.iosFramework), - throwsA(isA()), + command.getPatcher(ReleaseType.iosFramework), + isA(), ); }); }); @@ -296,6 +302,7 @@ void main() { verifyInOrder([ () => patcher.assertPreconditions(), () => patcher.assertArgsAreValid(), + () => cache.updateAll(), () => codePushClientWrapper.getApp(appId: appId), () => codePushClientWrapper.getRelease( appId: appId, @@ -347,6 +354,7 @@ void main() { verifyInOrder([ () => patcher.assertPreconditions(), () => patcher.assertArgsAreValid(), + () => cache.updateAll(), () => codePushClientWrapper.getApp(appId: appId), () => patcher.buildPatchArtifact(), () => patcher.extractReleaseVersionFromArtifact(any()), @@ -419,6 +427,19 @@ void main() { () => patcher.buildPatchArtifact(), ]); }); + + test('updates cache with both default and release Flutter revisions', + () async { + await runWithOverrides(command.run); + + verifyInOrder([ + cache.updateAll, + () => shorebirdEnv.copyWith( + flutterRevisionOverride: releaseFlutterRevision, + ), + cache.updateAll, + ]); + }); }); }); diff --git a/packages/shorebird_cli/test/src/commands/release_new/aar_releaser_test.dart b/packages/shorebird_cli/test/src/commands/release_new/aar_releaser_test.dart index 9a92a3bb..78ebb867 100644 --- a/packages/shorebird_cli/test/src/commands/release_new/aar_releaser_test.dart +++ b/packages/shorebird_cli/test/src/commands/release_new/aar_releaser_test.dart @@ -197,7 +197,7 @@ void main() { }); group('assertArgsAreValid', () { - group('when split-per-abi is true', () { + group('when release-version was not provided', () { setUp(() { when(() => argResults.wasParsed('release-version')).thenReturn(false); }); diff --git a/packages/shorebird_cli/test/src/commands/release_new/ios_framework_releaser_test.dart b/packages/shorebird_cli/test/src/commands/release_new/ios_framework_releaser_test.dart index 8eb6668c..622f366c 100644 --- a/packages/shorebird_cli/test/src/commands/release_new/ios_framework_releaser_test.dart +++ b/packages/shorebird_cli/test/src/commands/release_new/ios_framework_releaser_test.dart @@ -256,7 +256,6 @@ void main() { 'ios', 'framework', 'Release', - 'Flutter.xcframework', ), ), ); diff --git a/packages/shorebird_cli/test/src/commands/release_new/release_new_command_test.dart b/packages/shorebird_cli/test/src/commands/release_new/release_new_command_test.dart index 770bfc0e..5f766c7f 100644 --- a/packages/shorebird_cli/test/src/commands/release_new/release_new_command_test.dart +++ b/packages/shorebird_cli/test/src/commands/release_new/release_new_command_test.dart @@ -2,6 +2,7 @@ import 'package:args/args.dart'; import 'package:mason_logger/mason_logger.dart'; import 'package:mocktail/mocktail.dart'; import 'package:scoped/scoped.dart'; +import 'package:shorebird_cli/src/cache.dart'; import 'package:shorebird_cli/src/code_push_client_wrapper.dart'; import 'package:shorebird_cli/src/commands/release_new/release_new.dart'; import 'package:shorebird_cli/src/config/config.dart'; @@ -42,6 +43,7 @@ void main() { ); late ArgResults argResults; + late Cache cache; late CodePushClientWrapper codePushClientWrapper; late Directory shorebirdRoot; late Directory projectRoot; @@ -57,6 +59,7 @@ void main() { return runScoped( body, values: { + cacheRef.overrideWith(() => cache), codePushClientWrapperRef.overrideWith(() => codePushClientWrapper), loggerRef.overrideWith(() => logger), shorebirdEnvRef.overrideWith(() => shorebirdEnv), @@ -77,6 +80,7 @@ void main() { setUp(() { argResults = MockArgResults(); + cache = MockCache(); codePushClientWrapper = MockCodePushClientWrapper(); logger = MockShorebirdLogger(); progress = MockProgress(); @@ -90,6 +94,8 @@ void main() { when(() => argResults['platform']).thenReturn(['android']); when(() => argResults.wasParsed(any())).thenReturn(true); + when(cache.updateAll).thenAnswer((_) async => {}); + when(() => codePushClientWrapper.getApp(appId: any(named: 'appId'))) .thenAnswer((_) async => appMetadata); when( @@ -204,6 +210,7 @@ void main() { verifyInOrder([ releaser.assertPreconditions, releaser.assertArgsAreValid, + cache.updateAll, () => codePushClientWrapper.getApp(appId: appId), releaser.buildReleaseArtifacts, () => releaser.getReleaseVersion( @@ -291,6 +298,7 @@ Note: ${lightCyan.wrap('shorebird patch --platform=android')} without the --rele verifyInOrder([ releaser.assertPreconditions, releaser.assertArgsAreValid, + cache.updateAll, () => codePushClientWrapper.getApp(appId: appId), releaser.buildReleaseArtifacts, () => releaser.getReleaseVersion(