refactor(shorebird_cli): upgrade analysis_options (#2720)

This commit is contained in:
Felix Angelov
2024-12-19 14:55:35 -06:00
committed by GitHub
parent 82dfa29995
commit 1b388456f4
73 changed files with 509 additions and 197 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
include: package:very_good_analysis/analysis_options.5.1.0.yaml
include: package:very_good_analysis/analysis_options.7.0.0.yaml
analyzer:
exclude:
- lib/**.g.dart
@@ -1,10 +0,0 @@
/// shorebird_cli, The shorebird command-line tool
///
/// ```sh
/// # activate shorebird_cli
/// dart pub global activate shorebird_cli
///
/// # see usage
/// shorebird --help
/// ```
library shorebird_cli;
@@ -82,7 +82,7 @@ final artifactBuilderRef = create(ArtifactBuilder.new);
ArtifactBuilder get artifactBuilder => read(artifactBuilderRef);
extension on String {
/// Converts this base64-encoded public key into the Map<String, String>:
/// Converts this base64-encoded public key into the `Map<String, String>`:
/// {'SHOREBIRD_PUBLIC_KEY': this}
///
/// SHOREBIRD_PUBLIC_KEY is the name expected by the Shorebird's Flutter tool
@@ -1,4 +1,3 @@
// ignore_for_file: public_member_api_docs
// cspell:words googleapis bryanoltman endtemplate CLI tgvek orctktiabrek
// cspell:words GOCSPX googleusercontent Pkkwp Entra
import 'dart:convert';
@@ -162,7 +161,7 @@ class AuthenticatedClient extends http.BaseClient {
client,
authEndpoints: authEndpoints,
);
} catch (e, s) {
} on Exception catch (e, s) {
logger
..err('Failed to refresh credentials.')
..info(
@@ -328,7 +327,9 @@ class Auth {
json.decode(contents) as Map<String, dynamic>,
);
_email = _credentials?.email;
} catch (_) {}
} on Exception {
// Swallow json decode exceptions.
}
}
}
@@ -365,7 +366,7 @@ extension JwtClaims on oauth2.AccessCredentials {
final Jwt jwt;
try {
jwt = Jwt.parse(token);
} catch (_) {
} on Exception {
return null;
}
@@ -409,12 +410,15 @@ extension OauthAuthProvider on Jwt {
}
}
/// Extension on [AuthProvider] which exposes OAuth 2.0 values.
extension OauthValues on AuthProvider {
/// The OAuth 2.0 endpoints for the provider.
oauth2.AuthEndpoints get authEndpoints => switch (this) {
(AuthProvider.google) => const oauth2.GoogleAuthEndpoints(),
(AuthProvider.microsoft) => MicrosoftAuthEndpoints(),
};
/// The OAuth 2.0 client ID for the provider.
oauth2.ClientId get clientId {
switch (this) {
case AuthProvider.google:
@@ -442,6 +446,7 @@ extension OauthValues on AuthProvider {
}
}
/// The OAuth 2.0 scopes for the provider.
List<String> get scopes => switch (this) {
(AuthProvider.google) => [
'openid',
+31 -4
View File
@@ -1,5 +1,3 @@
// ignore_for_file: public_member_api_docs
import 'dart:io' hide Platform;
import 'package:http/http.dart' as http;
@@ -32,10 +30,10 @@ class CacheUpdateFailure implements Exception {
String toString() => 'CacheUpdateFailure: $message';
}
// A reference to a [Cache] instance.
/// A reference to a [Cache] instance.
final cacheRef = create(Cache.new);
// The [Cache] instance available in the current zone.
/// The [Cache] instance available in the current zone.
Cache get cache => read(cacheRef);
/// {@template cache}
@@ -46,12 +44,14 @@ Cache get cache => read(cacheRef);
/// [ShorebirdArtifacts] since uses the current Shorebird environment.
/// {@endtemplate}
class Cache {
/// {@macro cache}
Cache() {
registerArtifact(PatchArtifact(cache: this, platform: platform));
registerArtifact(BundleToolArtifact(cache: this, platform: platform));
registerArtifact(AotToolsArtifact(cache: this, platform: platform));
}
/// Register a new [CachedArtifact] with the cache.
void registerArtifact(CachedArtifact artifact) => _artifacts.add(artifact);
/// Update all artifacts in the cache.
@@ -119,10 +119,13 @@ class Cache {
final List<CachedArtifact> _artifacts = [];
/// The storage base url.
String get storageBaseUrl => 'https://storage.googleapis.com';
/// The storage bucket host.
String get storageBucket => 'download.shorebird.dev';
/// Clear the cache.
Future<void> clear() async {
final cacheDir = shorebirdCacheDirectory;
if (cacheDir.existsSync()) {
@@ -131,10 +134,17 @@ class Cache {
}
}
/// {@template cached_artifact}
/// An artifact which is cached by Shorebird.
/// {@endtemplate}
abstract class CachedArtifact {
/// {@macro cached_artifact}
CachedArtifact({required this.cache, required this.platform});
/// The cache instance to use.
final Cache cache;
/// The platform to use.
final Platform platform;
/// The on-disk name of the artifact.
@@ -156,6 +166,7 @@ abstract class CachedArtifact {
/// is assumed to be correct.
String? get checksum;
/// Extract the artifact from the provided [stream] to the [outputPath].
Future<void> extractArtifact(http.ByteStream stream, String outputPath) {
final file = File(p.join(outputPath, fileName))
..createSync(recursive: true);
@@ -169,6 +180,7 @@ abstract class CachedArtifact {
/// Used to validate that the artifact was fully downloaded and extracted.
File get stampFile => File('${file.path}.stamp');
/// Whether the artifact is valid (has a matching checksum).
Future<bool> isValid() async {
if (!file.existsSync() || !stampFile.existsSync()) {
return false;
@@ -184,6 +196,7 @@ abstract class CachedArtifact {
return checksumChecker.checkFile(file, checksum!);
}
/// Re-fetch the artifact from the storage URL.
Future<void> update() async {
// Clear any existing artifact files.
await _delete();
@@ -271,7 +284,12 @@ allowed to access $storageUrl.''',
}
}
/// {@template aot_tools_artifact}
/// The aot_tools.dill artifact.
/// Used for linking and generating optimized AOT snapshots.
/// {@endtemplate}
class AotToolsArtifact extends CachedArtifact {
/// {@macro aot_tools_artifact}
AotToolsArtifact({required super.cache, required super.platform});
@override
@@ -301,7 +319,11 @@ class AotToolsArtifact extends CachedArtifact {
String? get checksum => null;
}
/// {@template patch_artifact}
/// The patch artifact which is used to apply binary patches.
/// {@endtemplate}
class PatchArtifact extends CachedArtifact {
/// {@macro patch_artifact}
PatchArtifact({required super.cache, required super.platform});
@override
@@ -342,7 +364,12 @@ class PatchArtifact extends CachedArtifact {
String? get checksum => null;
}
/// {@template bundle_tool_artifact}
/// The bundletool.jar artifact.
/// Used for interacting with Android app bundles (aab).
/// {@endtemplate}
class BundleToolArtifact extends CachedArtifact {
/// {@macro bundle_tool_artifact}
BundleToolArtifact({required super.cache, required super.platform});
@override
@@ -1,3 +1,4 @@
// TODO(felangel): Add public member API docs and remove the ignore.
// ignore_for_file: public_member_api_docs
// cspell:words endtemplate pubspec sideloadable bryanoltman archs sideload
// cspell:words xcarchive codesigned xcframework
@@ -87,11 +88,7 @@ class CodePushClientWrapper {
}) async {
late final String displayName;
if (appName == null) {
String? defaultAppName;
try {
defaultAppName = shorebirdEnv.getPubspecYaml()?.name;
} catch (_) {}
final defaultAppName = shorebirdEnv.getPubspecYaml()?.name;
displayName = logger.prompt(
'${lightGreen.wrap('?')} How should we refer to this app?',
defaultValue: defaultAppName,
@@ -93,10 +93,9 @@ Android Toolchain
• Gradle: ${gradlewVersion ?? notDetected}''');
}
logger.info(output.toString());
// ignore: cascade_invocations
logger.info('URL Reachability');
logger
..info(output.toString())
..info('URL Reachability');
await networkChecker.checkReachability();
logger.info('');
@@ -111,7 +110,7 @@ Android Toolchain
);
} on NetworkCheckerException catch (error) {
uploadProgress.fail('GCP upload speed test failed: ${error.message}');
} catch (error) {
} on Exception catch (error) {
uploadProgress.fail('GCP upload speed test failed: $error');
}
@@ -127,7 +126,7 @@ Android Toolchain
downloadProgress.fail(
'GCP download speed test failed: ${error.message}',
);
} catch (error) {
} on Exception catch (error) {
downloadProgress.fail(
'GCP download speed test failed: $error',
);
@@ -143,7 +142,7 @@ Android Toolchain
Future<String?> _tryGetFlutterVersion() async {
try {
return await shorebirdFlutter.getVersionString();
} catch (error) {
} on Exception catch (error) {
logger.detail('Unable to determine Flutter version.\n$error');
return null;
}
@@ -34,7 +34,7 @@ class FlutterVersionsListCommand extends ShorebirdCommand {
try {
versions = await shorebirdFlutter.getVersions();
progress.cancel();
} catch (error) {
} on Exception catch (error) {
progress.fail('Failed to fetch Flutter versions.');
logger.err('$error');
return ExitCode.software.code;
@@ -60,7 +60,7 @@ Please make sure you are running "shorebird init" from within your Flutter proje
''');
return ExitCode.noInput.code;
}
} catch (error) {
} on Exception catch (error) {
logger.err('Error parsing "pubspec.yaml": $error');
return ExitCode.software.code;
}
@@ -108,7 +108,7 @@ Please make sure you are running "shorebird init" from within your Flutter proje
logger.info(' - $flavor');
}
}
} catch (error) {
} on Exception catch (error) {
detectFlavorsProgress.fail();
logger.err('Unable to extract product flavors.\n$error');
return ExitCode.software.code;
@@ -138,7 +138,7 @@ Please make sure you are running "shorebird init" from within your Flutter proje
existingApp = await codePushClientWrapper.getApp(
appId: shorebirdYaml!.appId,
);
} catch (e) {
} on Exception catch (e) {
updateShorebirdYamlProgress.fail('Failed to get existing app info: $e');
return ExitCode.software.code;
}
@@ -227,7 +227,7 @@ Please make sure you are running "shorebird init" from within your Flutter proje
flavors = values;
appId = flavors.values.first;
}
} catch (error) {
} on Exception catch (error) {
logger.err('$error');
return ExitCode.software.code;
}
@@ -1,5 +1,3 @@
// ignore_for_file: public_member_api_docs
import 'package:mason_logger/mason_logger.dart';
import 'package:shorebird_cli/src/auth/auth.dart';
import 'package:shorebird_cli/src/logging/logging.dart';
@@ -12,6 +10,7 @@ import 'package:shorebird_code_push_protocol/shorebird_code_push_protocol.dart'
/// Login as a CI user.
/// {@endtemplate}
class LoginCiCommand extends ShorebirdCommand {
/// {@macro login_ci_command}
LoginCiCommand() {
argParser.addOption(
'provider',
@@ -54,7 +53,7 @@ We could not find a Shorebird account for ${error.email}.''',
'''If you have not yet created an account, go to "${link(uri: Uri.parse('https://console.shorebird.dev'))}" to create one. If you believe this is an error, please reach out to us via Discord, we're happy to help!''',
);
return ExitCode.software.code;
} catch (error) {
} on Exception catch (error) {
logger.err(error.toString());
return ExitCode.software.code;
}
@@ -72,6 +71,7 @@ ${lightCyan.wrap('export $shorebirdTokenEnvVar="\$SHOREBIRD_TOKEN" && shorebird
return ExitCode.success.code;
}
/// Prompt the user to visit the provided [url] to authorize the CLI.
void prompt(String url) {
logger.info('''
The Shorebird CLI needs your authorization to manage apps, releases, and patches on your behalf.
@@ -61,7 +61,7 @@ We could not find a Shorebird account for ${error.email}.''',
"""If you have not yet created an account, you can do so at "${link(uri: consoleUri)}". If you believe this is an error, please reach out to us via Discord, we're happy to help!""",
);
return ExitCode.software.code;
} catch (error) {
} on Exception catch (error) {
logger.err(error.toString());
return ExitCode.software.code;
}
@@ -125,7 +125,7 @@ class AarPatcher extends Patcher {
Uri.parse(releaseArtifact.value.url),
);
releaseArtifactPaths[releaseArtifact.key] = releaseArtifactFile.path;
} catch (error) {
} on Exception catch (error) {
downloadReleaseArtifactProgress.fail('$error');
throw ProcessExit(ExitCode.software.code);
}
@@ -163,7 +163,7 @@ class AarPatcher extends Patcher {
hash: hash,
size: await File(diffPath).length(),
);
} catch (error) {
} on Exception catch (error) {
createDiffProgress.fail('$error');
throw ProcessExit(ExitCode.software.code);
}
@@ -176,7 +176,7 @@ Please refer to ${link(uri: Uri.parse('https://github.com/shorebirdtech/shorebir
message: 'Downloading release artifact ${i + 1}/$numArtifacts',
);
releaseArtifactPaths[releaseArtifact.key] = releaseArtifactFile.path;
} catch (error) {
} on Exception {
throw ProcessExit(ExitCode.software.code);
}
}
@@ -227,7 +227,7 @@ Please refer to ${link(uri: Uri.parse('https://github.com/shorebirdtech/shorebir
size: await File(diffPath).length(),
hashSignature: hashSignature,
);
} catch (error) {
} on Exception catch (error) {
createDiffProgress.fail('$error');
throw ProcessExit(ExitCode.software.code);
}
@@ -131,7 +131,7 @@ class IosFrameworkPatcher extends Patcher {
genSnapshotArtifact: ShorebirdArtifact.genSnapshotIos,
additionalArgs: IosPatcher.splitDebugInfoArgs(splitDebugInfoPath),
);
} catch (error) {
} on Exception catch (error) {
buildProgress.fail('$error');
throw ProcessExit(ExitCode.software.code);
}
@@ -259,7 +259,7 @@ class IosFrameworkPatcher extends Patcher {
releaseSnapshot: releaseArtifactFile,
);
patchBaseProgress.complete();
} catch (error) {
} on Exception catch (error) {
patchBaseProgress.fail('$error');
throw ProcessExit(ExitCode.software.code);
}
@@ -339,7 +339,7 @@ class IosFrameworkPatcher extends Patcher {
workingDirectory: buildDirectory.path,
additionalArgs: IosPatcher.splitDebugInfoArgs(splitDebugInfoPath),
);
} catch (error) {
} on Exception catch (error) {
linkProgress.fail('Failed to link AOT files: $error');
throw ProcessExit(ExitCode.software.code);
}
@@ -1,5 +1,3 @@
// ignore_for_file: public_member_api_docs
import 'dart:async';
import 'dart:io';
@@ -79,6 +77,7 @@ class IosPatcher extends Patcher {
return p.join(p.absolute(directory), splitDebugInfoFileName);
}
/// The last build's link percentage.
@visibleForTesting
double? lastBuildLinkPercentage;
@@ -227,7 +226,7 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}''',
ipaBuildResult.kernelFile.copySync(_appDillCopyPath);
buildProgress.complete();
} catch (_) {
} on Exception {
throw ProcessExit(ExitCode.software.code);
}
@@ -354,7 +353,7 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}''',
releaseSnapshot: releaseArtifactFile,
);
patchBaseProgress.complete();
} catch (error) {
} on Exception catch (error) {
patchBaseProgress.fail('$error');
throw ProcessExit(ExitCode.software.code);
}
@@ -407,7 +406,7 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}''',
final plist = Plist(file: plistFile);
try {
return plist.versionNumber;
} catch (error) {
} on Exception catch (error) {
logger.err(
'Failed to determine release version from ${plistFile.path}: $error',
);
@@ -478,7 +477,7 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}''',
dumpDebugInfoPath: dumpDebugInfoDir?.path,
additionalArgs: splitDebugInfoArgs(splitDebugInfoPath),
);
} catch (error) {
} on Exception catch (error) {
linkProgress.fail('Failed to link AOT files: $error');
return (exitCode: ExitCode.software.code, linkPercentage: null);
} finally {
@@ -228,7 +228,7 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}''',
macosBuildResult.kernelFile.copySync(_appDillCopyPath);
buildProgress.complete();
} catch (_) {
} on Exception {
throw ProcessExit(ExitCode.software.code);
}
@@ -345,7 +345,7 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}''',
releaseSnapshot: releaseArtifactFile,
);
patchBaseProgress.complete();
} catch (error) {
} on Exception catch (error) {
patchBaseProgress.fail('$error');
throw ProcessExit(ExitCode.software.code);
}
@@ -396,7 +396,7 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}''',
final plist = Plist(file: plistFile);
try {
return plist.versionNumber;
} catch (error) {
} on Exception catch (error) {
logger.err(
'Failed to determine release version from ${plistFile.path}: $error',
);
@@ -467,7 +467,7 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}''',
dumpDebugInfoPath: dumpDebugInfoDir?.path,
additionalArgs: splitDebugInfoArgs(splitDebugInfoPath),
);
} catch (error) {
} on Exception catch (error) {
linkProgress.fail('Failed to link AOT files: $error');
return (exitCode: ExitCode.software.code, linkPercentage: null);
} finally {
@@ -1,5 +1,3 @@
// ignore_for_file: public_member_api_docs
import 'dart:io';
import 'package:collection/collection.dart';
@@ -29,9 +27,15 @@ import 'package:shorebird_cli/src/third_party/flutter_tools/lib/flutter_tools.da
import 'package:shorebird_cli/src/version.dart';
import 'package:shorebird_code_push_client/shorebird_code_push_client.dart';
/// Signature for a function that returns a [Patcher] for a given [ReleaseType].
typedef ResolvePatcher = Patcher Function(ReleaseType releaseType);
/// {@template patch_command}
/// A command that creates a shorebird patch for the provided target platforms.
/// `shorebird patch --platforms=android,ios`
/// {@endtemplate}
class PatchCommand extends ShorebirdCommand {
/// {@macro patch_command}
PatchCommand({
ResolvePatcher? resolvePatcher,
}) {
@@ -142,10 +146,12 @@ of the iOS app that is using this module.''',
);
}
/// Warning message for when native code diffs are detected.
static final allowNativeDiffsHelpText = '''
Patch even if native code diffs are detected.
NOTE: this is ${styleBold.wrap('not')} recommended. Native code changes cannot be included in a patch and attempting to do so can cause your app to crash or behave unexpectedly.''';
/// Warning message for when asset diffs are detected.
static final allowAssetDiffsHelpText = '''
Patch even if asset diffs are detected.
NOTE: this is ${styleBold.wrap('not')} recommended. Asset changes cannot be included in a patch can cause your app to behave unexpectedly.''';
@@ -177,8 +183,10 @@ NOTE: this is ${styleBold.wrap('not')} recommended. Asset changes cannot be incl
/// Whether --no-confirm was passed.
bool get noConfirm => results['no-confirm'] == true;
/// Whether the patch is for the staging environment.
bool get isStaging => track == DeploymentTrack.staging;
/// The deployment track to publish the patch to.
DeploymentTrack get track {
final channel = results['track'] as String;
return DeploymentTrack.values.firstWhere((t) => t.channel == channel);
@@ -207,6 +215,7 @@ NOTE: this is ${styleBold.wrap('not')} recommended. Asset changes cannot be incl
return ExitCode.success.code;
}
/// Returns a [Patcher] for the given [ReleaseType].
@visibleForTesting
Patcher getPatcher(ReleaseType releaseType) {
switch (releaseType) {
@@ -248,8 +257,10 @@ NOTE: this is ${styleBold.wrap('not')} recommended. Asset changes cannot be incl
}
}
/// The last built Flutter revision.
String? lastBuiltFlutterRevision;
/// Creates a patch using the provided [patcher].
@visibleForTesting
Future<void> createPatch(Patcher patcher) async {
await patcher.assertPreconditions();
@@ -291,7 +302,7 @@ NOTE: this is ${styleBold.wrap('not')} recommended. Asset changes cannot be incl
try {
await shorebirdFlutter.installRevision(revision: release.flutterRevision);
} catch (_) {
} on Exception {
throw ProcessExit(ExitCode.software.code);
}
@@ -395,6 +406,7 @@ NOTE: this is ${styleBold.wrap('not')} recommended. Asset changes cannot be incl
);
}
/// Prompts the user for the specific release to patch.
Future<Release> promptForRelease() async {
final releases = await codePushClientWrapper.getReleases(
appId: appId,
@@ -414,6 +426,7 @@ NOTE: this is ${styleBold.wrap('not')} recommended. Asset changes cannot be incl
);
}
/// Asserts that the release contains a platform for the given [patcher].
void assertReleaseContainsPlatform({
required Release release,
required Patcher patcher,
@@ -429,6 +442,7 @@ NOTE: this is ${styleBold.wrap('not')} recommended. Asset changes cannot be incl
}
}
/// Asserts that the provided [release] is active.
void assertReleaseIsActive({
required Release release,
required Patcher patcher,
@@ -443,6 +457,7 @@ Please re-run the release command for this version or create a new release.''');
}
}
/// Ensures the diff between the release and patch archives is safe to patch.
Future<DiffStatus> assertUnpatchableDiffs({
required ReleaseArtifact releaseArtifact,
required File patchArchive,
@@ -463,6 +478,7 @@ Please re-run the release command for this version or create a new release.''');
}
}
/// Confirms the patch creation (including a summary).
Future<void> confirmCreatePatch({
required AppMetadata app,
required String releaseVersion,
@@ -511,6 +527,7 @@ ${summary.join('\n')}
}
}
/// Downloads the given [releaseArtifact].
Future<File> downloadReleaseArtifact({
required ReleaseArtifact releaseArtifact,
}) async {
@@ -520,7 +537,7 @@ ${summary.join('\n')}
Uri.parse(releaseArtifact.url),
message: 'Downloading ${releaseArtifact.arch}',
);
} catch (_) {
} on Exception {
throw ProcessExit(ExitCode.software.code);
}
@@ -1,5 +1,3 @@
// ignore_for_file: public_member_api_docs
import 'dart:io';
import 'package:args/args.dart';
@@ -31,7 +29,7 @@ abstract class Patcher {
required this.target,
});
// Link percentage that is considered the minimum before a user might notice.
/// Link percentage that is considered the minimum before a user might notice.
static const double minLinkPercentage = 75;
/// The standard link percentage warning.
@@ -1,4 +1,3 @@
// ignore_for_file: public_member_api_docs
// cspell:words devicectl endtemplate bryanoltman sideloadable previewable apks
// cspell:words bundletool
import 'dart:async';
@@ -252,6 +251,7 @@ class PreviewCommand extends ShorebirdCommand {
};
}
/// Prompts the user to choose an app to preview.
Future<String?> promptForApp() async {
final apps = await codePushClientWrapper.getApps();
if (apps.isEmpty) return null;
@@ -263,6 +263,7 @@ class PreviewCommand extends ShorebirdCommand {
return app.appId;
}
/// Prompts the user to choose a release version to preview.
Future<String?> promptForReleaseVersion(List<Release> releases) async {
if (releases.isEmpty) return null;
final release = logger.chooseOne(
@@ -273,6 +274,7 @@ class PreviewCommand extends ShorebirdCommand {
return release.version;
}
/// Prompts the user to choose a platform to preview.
Future<ReleasePlatform> promptForPlatform(
List<ReleasePlatform> platforms,
) async {
@@ -284,6 +286,7 @@ class PreviewCommand extends ShorebirdCommand {
return ReleasePlatform.values.firstWhere((p) => p.displayName == platform);
}
/// Installs and launches the release on macOS.
Future<int> installAndLaunchMacos({
required String appId,
required Release release,
@@ -300,7 +303,7 @@ class PreviewCommand extends ShorebirdCommand {
arch: 'app',
platform: platform,
);
} catch (e, s) {
} on Exception catch (e, s) {
logger
..err('Error getting release artifact: $e')
..detail('Stack trace: $s');
@@ -332,7 +335,7 @@ class PreviewCommand extends ShorebirdCommand {
destination: appDirectory.path,
);
downloadArtifactProgress.complete();
} catch (error) {
} on Exception catch (error) {
downloadArtifactProgress.fail('$error');
return ExitCode.software.code;
}
@@ -349,6 +352,7 @@ class PreviewCommand extends ShorebirdCommand {
return completer.future.then((_) => ExitCode.success.code);
}
/// Installs and launches the release on Android.
Future<int> installAndLaunchAndroid({
required String appId,
required Release release,
@@ -368,7 +372,7 @@ class PreviewCommand extends ShorebirdCommand {
arch: 'aab',
platform: platform,
);
} catch (e, s) {
} on Exception catch (e, s) {
logger
..err('Error getting release artifact: $e')
..detail('Stack trace: $s');
@@ -396,7 +400,7 @@ class PreviewCommand extends ShorebirdCommand {
}
downloadArtifactProgress.complete();
} catch (error) {
} on Exception catch (error) {
downloadArtifactProgress.fail('$error');
return ExitCode.software.code;
}
@@ -414,7 +418,7 @@ class PreviewCommand extends ShorebirdCommand {
try {
await setChannelOnAab(aabFile: aabFile, channel: track.channel);
progress.complete();
} catch (error) {
} on Exception catch (error) {
progress.fail('$error');
return ExitCode.software.code;
}
@@ -424,7 +428,7 @@ class PreviewCommand extends ShorebirdCommand {
try {
package = await bundletool.getPackageName(aabFile.path);
extractMetadataProgress.complete();
} catch (error) {
} on Exception catch (error) {
extractMetadataProgress.fail('$error');
return ExitCode.software.code;
}
@@ -434,7 +438,7 @@ class PreviewCommand extends ShorebirdCommand {
await bundletool.buildApks(bundle: aabFile.path, output: apksPath);
final apksLink = link(uri: Uri.parse(apksPath));
buildApksProgress.complete('Built apks: ${cyan.wrap(apksLink)}');
} catch (error) {
} on Exception catch (error) {
buildApksProgress.fail('$error');
return ExitCode.software.code;
}
@@ -443,7 +447,7 @@ class PreviewCommand extends ShorebirdCommand {
try {
await bundletool.installApks(apks: apksPath, deviceId: deviceId);
installApksProgress.complete();
} catch (error) {
} on Exception catch (error) {
installApksProgress.fail('$error');
return ExitCode.software.code;
}
@@ -453,7 +457,7 @@ class PreviewCommand extends ShorebirdCommand {
await adb.clearAppData(package: package, deviceId: deviceId);
await adb.startApp(package: package, deviceId: deviceId);
startAppProgress.complete();
} catch (error) {
} on Exception catch (error) {
startAppProgress.fail('$error');
return ExitCode.software.code;
}
@@ -469,6 +473,7 @@ class PreviewCommand extends ShorebirdCommand {
return process.exitCode;
}
/// Installs and launches the release on iOS.
Future<int> installAndLaunchIos({
required String appId,
required Release release,
@@ -488,7 +493,7 @@ class PreviewCommand extends ShorebirdCommand {
arch: 'runner',
platform: platform,
);
} catch (e, s) {
} on Exception catch (e, s) {
logger
..err('Error getting release artifact: $e')
..detail('Stack trace: $s');
@@ -520,7 +525,7 @@ class PreviewCommand extends ShorebirdCommand {
outputDirectory: runnerDirectory,
);
downloadArtifactProgress.complete();
} catch (error) {
} on Exception catch (error) {
downloadArtifactProgress.fail('$error');
return ExitCode.software.code;
}
@@ -533,7 +538,7 @@ class PreviewCommand extends ShorebirdCommand {
channel: track.channel,
);
progress.complete();
} catch (error) {
} on Exception catch (error) {
progress.fail('$error');
return ExitCode.software.code;
}
@@ -576,12 +581,13 @@ class PreviewCommand extends ShorebirdCommand {
}
return installExitCode;
} catch (error, stackTrace) {
} on Exception catch (error, stackTrace) {
logger.detail('Error launching app. $error $stackTrace');
return ExitCode.software.code;
}
}
/// Resolves the artifact path for the given parameters.
String getArtifactPath({
required String appId,
required Release release,
@@ -695,7 +701,10 @@ class PreviewCommand extends ShorebirdCommand {
}
}
/// Extension on [Release] that exposes the active platforms (e.g. platforms
/// that can be previewed).
extension Previewable on Release {
/// Returns the platforms that can be previewed.
List<ReleasePlatform> get activePlatforms => platformStatuses.entries
.where((e) => e.value == ReleaseStatus.active)
.map((e) => e.key)
@@ -84,7 +84,7 @@ class AarReleaser extends Releaser {
targetPlatforms: architectures,
args: argResults.forwardedArgs,
);
} catch (e) {
} on Exception catch (e) {
logger.err('Failed to build aar: $e');
throw ProcessExit(ExitCode.software.code);
}
@@ -159,7 +159,7 @@ Please comment and upvote ${link(uri: Uri.parse('https://github.com/shorebirdtec
releaseArtifactRoot.path,
);
releaseVersionProgress.complete('Release version: $releaseVersion');
} catch (error) {
} on Exception catch (error) {
releaseVersionProgress.fail('$error');
throw ProcessExit(ExitCode.software.code);
}
@@ -99,7 +99,7 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}''',
try {
await artifactBuilder.buildIosFramework(args: argResults.forwardedArgs);
} catch (error) {
} on Exception catch (error) {
buildProgress.fail('Failed to build iOS framework: $error');
throw ProcessExit(ExitCode.software.code);
}
@@ -165,7 +165,7 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}''',
try {
return Plist(file: plistFile).versionNumber;
} catch (error) {
} on Exception catch (error) {
logger.err(
'''Failed to determine release version from ${plistFile.path}: $error''',
);
@@ -148,7 +148,7 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}''',
try {
return Plist(file: plistFile).versionNumber;
} catch (error) {
} on Exception catch (error) {
logger.err(
'''Failed to determine release version from ${plistFile.path}: $error''',
);
@@ -250,7 +250,7 @@ of the iOS app that is using this module. (aar and ios-framework only)''',
final targetFlutterRevision = await resolveTargetFlutterRevision();
try {
await shorebirdFlutter.installRevision(revision: targetFlutterRevision);
} catch (_) {
} on Exception {
throw ProcessExit(ExitCode.software.code);
}
@@ -331,7 +331,7 @@ of the iOS app that is using this module. (aar and ios-framework only)''',
revision = await shorebirdFlutter.resolveFlutterRevision(
flutterVersionArg!,
);
} catch (error) {
} on Exception catch (error) {
logger.err(
'''
Unable to determine revision for Flutter version: $flutterVersionArg.
@@ -115,7 +115,7 @@ class GetApksCommand extends ShorebirdCommand {
universal: results['universal'] as bool,
);
buildApksProgress.complete();
} catch (error) {
} on Exception catch (error) {
buildApksProgress.fail('$error');
return ExitCode.software.code;
}
@@ -178,7 +178,7 @@ class GetApksCommand extends ShorebirdCommand {
Uri.parse(releaseArtifact.url),
message: 'Downloading aab',
);
} catch (_) {
} on Exception catch (_) {
throw ProcessExit(ExitCode.software.code);
}
+1 -1
View File
@@ -69,7 +69,7 @@ class Doctor {
for (final issue in fixableIssues) {
try {
await issue.fix!();
} catch (error) {
} on Exception catch (error) {
failedFixes[issue] = error;
}
}
@@ -1,5 +1,3 @@
// ignore_for_file: public_member_api_docs
import 'package:scoped_deps/scoped_deps.dart';
/// A reference to an [EngineConfig] instance.
@@ -8,20 +6,30 @@ final engineConfigRef = create(() => const EngineConfig.empty());
/// The [EngineConfig] instance available in the current zone.
EngineConfig get engineConfig => read(engineConfigRef);
/// {@template engine_config}
/// An object that contains a local engine configuration.
/// {@endtemplate}
class EngineConfig {
/// {@macro engine_config}
const EngineConfig({
required this.localEngineSrcPath,
required this.localEngine,
required this.localEngineHost,
});
/// An empty [EngineConfig] instance.
const EngineConfig.empty()
: localEngineSrcPath = null,
localEngine = null,
localEngineHost = null;
/// The path to the local engine source.
final String? localEngineSrcPath;
/// The local engine name.
final String? localEngine;
/// The local engine host.
final String? localEngineHost;
@override
@@ -1,5 +1,3 @@
// ignore_for_file: public_member_api_docs
import 'package:json_annotation/json_annotation.dart';
import 'package:pub_semver/pub_semver.dart';
import 'package:shorebird_cli/src/extensions/version.dart';
@@ -15,6 +13,7 @@ part 'apple_device.g.dart';
/// {@macro apple_device}
class AppleDevice {
/// {@macro apple_device}
const AppleDevice({
required this.deviceProperties,
required this.hardwareProperties,
@@ -62,8 +61,12 @@ class AppleDevice {
String toString() => '$name ($osVersionString ${hardwareProperties.udid})';
}
/// {@template hardware_properties}
/// The hardware properties of a device.
/// {@endtemplate}
@JsonSerializable(createToJson: false, fieldRename: FieldRename.none)
class HardwareProperties {
/// {@macro hardware_properties}
const HardwareProperties({required this.platform, required this.udid});
/// The device's platform (e.g., "iOS").
@@ -72,12 +75,17 @@ class HardwareProperties {
/// The unique identifier of this device
final String udid;
/// Creates a [HardwareProperties] from [json].
static HardwareProperties fromJson(Json json) =>
_$HardwarePropertiesFromJson(json);
}
/// {@template device_properties}
/// The device properties for a given apple device.
/// {@endtemplate}
@JsonSerializable(createToJson: false, fieldRename: FieldRename.none)
class DeviceProperties {
/// {@macro device_properties}
const DeviceProperties({required this.name, this.osVersionNumber});
/// Human-readable name of the device (e.g., "Joe's iPhone").
@@ -86,12 +94,17 @@ class DeviceProperties {
/// The device's OS version as a string (e.g., "14.4.1").
final String? osVersionNumber;
/// Creates a [DeviceProperties] from [json].
static DeviceProperties fromJson(Json json) =>
_$DevicePropertiesFromJson(json);
}
/// {@template connection_properties}
/// The connection properties of a device.
/// {@endtemplate}
@JsonSerializable(createToJson: false, fieldRename: FieldRename.none)
class ConnectionProperties {
/// {@macro connection_properties}
const ConnectionProperties({required this.tunnelState, this.transportType});
/// How the device is connected. Values seen in development include
@@ -106,6 +119,7 @@ class ConnectionProperties {
/// - "unavailable" when the device is not connected via USB or wifi.
final String tunnelState;
/// Creates a [ConnectionProperties] from [json].
static ConnectionProperties fromJson(Json json) =>
_$ConnectionPropertiesFromJson(json);
}
@@ -1,4 +1,3 @@
// ignore_for_file: public_member_api_docs
import 'dart:async';
import 'dart:convert';
import 'dart:io';
@@ -15,6 +14,7 @@ import 'package:shorebird_cli/src/logging/logging.dart';
import 'package:shorebird_cli/src/shorebird_process.dart';
import 'package:shorebird_code_push_client/shorebird_code_push_client.dart';
/// Typedef for a bundle identifier string.
typedef BundleId = String;
/// {@template devicectl_exception}
@@ -48,10 +48,11 @@ Devicectl get devicectl => read(devicectlRef);
/// A wrapper around the `devicectl` command.
class Devicectl {
/// The executable name (`xcrun`).
static const executableName = 'xcrun';
static const baseArgs = [
'devicectl',
];
/// The base arguments for the `devicectl` command.
static const baseArgs = ['devicectl'];
/// Whether the `devicectl` command is available.
Future<bool> _isAvailable() async {
@@ -61,7 +62,7 @@ class Devicectl {
'--version',
]);
return result.exitCode == ExitCode.success.code;
} catch (_) {
} on Exception {
return false;
}
}
@@ -186,7 +187,7 @@ class Devicectl {
deviceId: device.udid,
runnerApp: runnerAppDirectory,
);
} catch (error) {
} on Exception catch (error) {
installProgress.fail('Failed to install app: $error');
return ExitCode.software.code;
}
@@ -195,7 +196,7 @@ class Devicectl {
final launchProgress = logger.progress('Launching app');
try {
await launchApp(deviceId: device.udid, bundleId: bundleId);
} catch (error) {
} on Exception catch (error) {
launchProgress.fail('Failed to launch app: $error');
return ExitCode.software.code;
}
@@ -1,5 +1,3 @@
// ignore_for_file: public_member_api_docs
import 'package:equatable/equatable.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:shorebird_code_push_client/shorebird_code_push_client.dart';
@@ -20,13 +18,19 @@ class NSError extends Equatable {
required this.userInfo,
});
/// The error code.
final int code;
/// The error domain.
final String domain;
/// Additional information about the error.
final UserInfo userInfo;
/// Creates an [NSError] from JSON.
static NSError fromJson(Json json) => _$NSErrorFromJson(json);
/// Converts this [NSError] to [Json].
Json toJson() => _$NSErrorToJson(this);
@override
@@ -45,8 +49,12 @@ NSError(
];
}
/// {@template user_info}
/// A pared-down representation of the userInfo property of an NSError.
/// {@endtemplate}
@JsonSerializable(fieldRename: FieldRename.none)
class UserInfo extends Equatable {
/// {@macro user_info}
const UserInfo({
this.description,
this.localizedDescription,
@@ -54,22 +62,29 @@ class UserInfo extends Equatable {
this.underlyingError,
});
/// A description of the error.
@JsonKey(name: 'NSDescription')
final StringContainer? description;
/// A localized description of the error.
@JsonKey(name: 'NSLocalizedDescription')
final StringContainer? localizedDescription;
/// A localized description of the failure reason.
@JsonKey(name: 'NSLocalizedFailureReason')
final StringContainer? localizedFailureReason;
/// The underlying error, if any.
@JsonKey(name: 'NSUnderlyingError')
final NSUnderlyingError? underlyingError;
/// An empty [UserInfo] instance.
static const nullInfo = UserInfo();
/// Creates a [UserInfo] from [Json].
static UserInfo fromJson(Json json) => _$UserInfoFromJson(json);
/// Converts this [UserInfo] to [Json].
Json toJson() => _$UserInfoToJson(this);
@override
@@ -90,14 +105,21 @@ UserInfo(
];
}
/// {@template string_container}
/// A container for a string value.
/// {@endtemplate}
@JsonSerializable(fieldRename: FieldRename.none)
class StringContainer extends Equatable {
/// {@macro string_container}
const StringContainer(this.string);
/// The string value.
final String string;
/// Creates a [StringContainer] from [Json].
static StringContainer fromJson(Json json) => _$StringContainerFromJson(json);
/// Converts this [StringContainer] to [Json].
Json toJson() => _$StringContainerToJson(this);
@override
@@ -107,15 +129,22 @@ class StringContainer extends Equatable {
List<Object> get props => [string];
}
/// {@template ns_underlying_error}
/// A pared-down representation of the NSUnderlyingError class.
/// {@endtemplate}
@JsonSerializable(fieldRename: FieldRename.none)
class NSUnderlyingError extends Equatable {
/// {@macro ns_underlying_error}
const NSUnderlyingError({required this.error});
/// The underlying error.
final NSError? error;
/// Creates an [NSUnderlyingError] from [Json].
static NSUnderlyingError fromJson(Json json) =>
_$NSUnderlyingErrorFromJson(json);
/// Converts this [NSUnderlyingError] to [Json].
Json toJson() => _$NSUnderlyingErrorToJson(this);
@override
@@ -1,5 +1,3 @@
// ignore_for_file: public_member_api_docs
import 'dart:io';
import 'package:collection/collection.dart';
@@ -12,7 +10,9 @@ import 'package:shorebird_cli/src/platform.dart';
import 'package:shorebird_cli/src/shorebird_documentation.dart';
import 'package:shorebird_cli/src/shorebird_process.dart';
/// Exception thrown when the Gradle version is incompatible.
class IncompatibleGradleException implements Exception {
/// The error pattern used to identify the exception.
static const errorPattern = 'Unsupported class file major version';
@override
@@ -50,15 +50,19 @@ To add android, run "flutter create . --platforms android"''';
}
}
/// {@template missing_gradle_wrapper_exception}
/// Thrown when the gradle wrapper cannot be found.
/// This has been resolved on the master channel but
/// on the stable channel currently creating an app via
/// `flutter create` does not generate a gradle wrapper which
/// means we're not able to accurately detect flavors until
/// the user has run `flutter build apk` at least once.
/// {@endtemplate}
class MissingGradleWrapperException implements Exception {
/// {@macro missing_gradle_wrapper_exception}
const MissingGradleWrapperException(this.executablePath);
/// The path to the gradle wrapper executable.
final String executablePath;
@override
@@ -77,6 +81,7 @@ Gradlew get gradlew => read(gradlewRef);
/// A wrapper around the gradle wrapper (gradlew).
class Gradlew {
/// The name of the executable.
String get executable => platform.isWindows ? 'gradlew.bat' : 'gradlew';
Future<ShorebirdProcessResult> _run(
@@ -63,6 +63,7 @@ class IDeviceSysLog {
/// idevicesyslog tails all logs produced by the device (similar to what is
/// shown in Console.app). This is very noisy and we only want to show logs
/// that are produced by the app. These log lines are of the form:
// ignore: unintended_html_in_doc_comment
/// Nov 10 14:46:57 Runner(Flutter)[1044] <Notice>: flutter: hello
static RegExp appLogLineRegex = RegExp(r'\(Flutter\)\[\d+\] <Notice>: (.*)$');
@@ -1,5 +1,3 @@
// ignore_for_file: public_member_api_docs
import 'dart:async';
import 'dart:convert';
import 'dart:io';
@@ -25,13 +23,18 @@ enum _DebuggerState {
attached,
}
/// {@template ios_deploy}
/// Wrapper around the `ios-deploy` command cached by the Flutter tool.
/// https://github.com/ios-control/ios-deploy
/// {@endtemplate}
class IOSDeploy {
IOSDeploy({ProcessSignal? sigint}) : _sigint = sigint ?? ProcessSignal.sigint;
/// {@macro ios_deploy}
const IOSDeploy({ProcessSignal? sigint})
: _sigint = sigint ?? ProcessSignal.sigint;
final ProcessSignal _sigint;
/// The location of the ios-deploy executable.
@visibleForTesting
static File get iosDeployExecutable => File(
p.join(
@@ -76,19 +79,25 @@ class IOSDeploy {
// Print backtrace for all threads while app is stopped.
static const String _backTraceAll = 'thread backtrace all';
// No provision profile errors.
/// No provision profile errors.
/// One of the possible errors when there is no provisioning profile.
static const noProvisioningProfileErrorOne = 'Error 0xe8008015';
/// Another possible error when there is no provisioning profile.
static const noProvisioningProfileErrorTwo = 'Error 0xe8000067';
// Device locked errors.
/// Device locked errors.
/// Error when the device is locked.
static const deviceLockedError = 'e80000e2';
/// Error message when the device is locked.
static const deviceLockedErrorMessage =
'the device was not, or could not be, unlocked';
// Unknown launch error.
/// Unknown launch error.
static const unknownAppLaunchError = 'Error 0xe8000022';
// Message when there is an unknown error.
/// Message when there is an unknown error.
static const unknownErrorFixInstructions = '''
Error launching app. Try launching from within Xcode via:
@@ -97,13 +106,13 @@ Error launching app. Try launching from within Xcode via:
Your Xcode version may be too old for your iOS version.
''';
// Message when the device is locked.
/// Message when the device is locked.
static const deviceLockedFixInstructions = '''
Your device is locked. Unlock your device first before running.
''';
// Message when there is no development team selected.
/// Message when there is no development team selected.
static const developmentTeamFixInstructions = '''
1- Open the Flutter project's Xcode target with
open ios/Runner.xcworkspace
@@ -297,7 +306,7 @@ Or run on an iOS simulator without code signing
unawaited(stdoutSubscription.cancel());
unawaited(stderrSubscription.cancel());
return status;
} catch (exception, stackTrace) {
} on Exception catch (exception, stackTrace) {
logger.detail('[ios-deploy] failed: $exception\n$stackTrace');
debuggerState = _DebuggerState.detached;
logger.err('[ios-deploy] failed: $exception');
@@ -328,8 +337,8 @@ Or run on an iOS simulator without code signing
}
}
// Handles interpreting stdout line and logs errors accordingly.
// Always returns the original line.
/// Handles interpreting stdout line and logs errors accordingly.
/// Always returns the original line.
@visibleForTesting
String detectFailures(String line, Logger logger) {
final isMissingProvisioningProfile =
@@ -1,4 +1,3 @@
// ignore_for_file: public_member_api_docs
import 'dart:io';
import 'package:args/args.dart';
@@ -11,6 +10,7 @@ import 'package:shorebird_cli/src/logging/logging.dart';
import 'package:shorebird_cli/src/release_type.dart';
import 'package:shorebird_cli/src/third_party/flutter_tools/lib/flutter_tools.dart';
/// Extension on [ArgResults] to make it easier to work with options.
extension OptionFinder on ArgResults {
/// Detects flags even when passed to underlying commands via a `--`
/// separator.
@@ -50,6 +50,7 @@ extension OptionFinder on ArgResults {
}
}
/// Extension on [ArgResults] to provide code signing related extensions.
extension CodeSign on ArgResults {
/// Asserts that either there is no public key argument
/// or that the path received exists.
@@ -101,6 +102,7 @@ extension FileArgs on ArgResults {
}
}
/// Extension on [ArgResults] to provide forwarded arguments.
extension ForwardedArgs on ArgResults {
bool _isPositionalArgPlatform(String arg) =>
ReleaseType.values.any((target) => target.cliName == arg);
@@ -1,10 +1,10 @@
// ignore_for_file: public_member_api_docs
/// Extension on [String] to provide null or empty getter.
extension NullOrEmpty on String? {
/// Returns `true` if this string is null or empty.
bool get isNullOrEmpty => this == null || this!.isEmpty;
}
/// Extension on [String] to provide Ansi escape code helpers.
extension AnsiEscapes on String {
/// Removes ANSI escape codes (usually the result of a lightCyan.wrap or
/// similar) from this string. Used to clean up
@@ -29,6 +29,7 @@ extension AnsiEscapes on String {
}
}
/// Extension on [String] to provide an `isUpperCase` getter.
extension IsUpperCase on String {
/// Returns `true` if this string is in uppercase.
bool isUpperCase() => this == toUpperCase();
@@ -17,10 +17,8 @@ Version? tryParseVersion(String versionString, {bool strict = true}) {
// and try again.
try {
return Version.parse('$versionString.0');
} catch (_) {
} on FormatException {
return null;
}
} catch (_) {
return null;
}
}
@@ -13,11 +13,11 @@ http.Client retryingHttpClient(http.Client client) => RetryClient(
/// Returns `true` if the [exception] is a retryable exception.
bool isRetryableException(Object exception, StackTrace _) {
return switch (exception.runtimeType) {
http.ClientException => true,
HttpException => true,
TlsException => true,
SocketException => true,
WebSocketException => true,
const (http.ClientException) => true,
const (HttpException) => true,
const (TlsException) => true,
const (SocketException) => true,
const (WebSocketException) => true,
_ => false,
};
}
@@ -47,11 +47,11 @@ class BuildEnvironmentMetadata extends Equatable {
);
// coverage:ignore-end
/// Converts a Map<String, dynamic> to a [BuildEnvironmentMetadata]
/// Converts a `Map<String, dynamic>` to a [BuildEnvironmentMetadata]
factory BuildEnvironmentMetadata.fromJson(Map<String, dynamic> json) =>
_$BuildEnvironmentMetadataFromJson(json);
/// Converts a [BuildEnvironmentMetadata] to a Map<String, dynamic>
/// Converts a [BuildEnvironmentMetadata] to a `Map<String, dynamic>`
Map<String, dynamic> toJson() => _$BuildEnvironmentMetadataToJson(this);
/// Creates a copy of this [BuildEnvironmentMetadata] with the given fields
@@ -51,11 +51,11 @@ class CreatePatchMetadata extends Equatable {
);
// coverage:ignore-end
/// Converts a Map<String, dynamic> to a [CreatePatchMetadata]
/// Converts a `Map<String, dynamic>` to a [CreatePatchMetadata]
factory CreatePatchMetadata.fromJson(Map<String, dynamic> json) =>
_$CreatePatchMetadataFromJson(json);
/// Converts a [CreatePatchMetadata] to a Map<String, dynamic>
/// Converts a [CreatePatchMetadata] to a `Map<String, dynamic>`
Map<String, dynamic> toJson() => _$CreatePatchMetadataToJson(this);
/// Returns a copy of this [CreatePatchMetadata] with the given fields
@@ -42,11 +42,11 @@ class UpdateReleaseMetadata extends Equatable {
);
// coverage:ignore-end
/// Converts a Map<String, dynamic> to a [UpdateReleaseMetadata].
/// Converts a `Map<String, dynamic>` to a [UpdateReleaseMetadata].
factory UpdateReleaseMetadata.fromJson(Map<String, dynamic> json) =>
_$UpdateReleaseMetadataFromJson(json);
/// Converts a [UpdateReleaseMetadata] to a Map<String, dynamic>.
/// Converts a [UpdateReleaseMetadata] to a `Map<String, dynamic>`.
Map<String, dynamic> toJson() => _$UpdateReleaseMetadataToJson(this);
/// Returns a copy of this [UpdateReleaseMetadata] with the given fields
@@ -51,7 +51,7 @@ class NetworkChecker {
try {
await httpClient.get(url);
progress.complete('$url ${lightGreen.wrap('OK')}');
} catch (e) {
} on Exception catch (e) {
progress.fail('$url unreachable');
logger.detail('Failed to reach $url: $e');
}
@@ -1,5 +1,3 @@
// ignore_for_file: public_member_api_docs
import 'dart:io';
import 'package:collection/collection.dart';
@@ -32,17 +30,24 @@ To add iOS, run "flutter create . --platforms ios"''';
/// Acceptable values can be found by running `flutter build ipa -h`.
/// {@endtemplate}
enum ExportMethod {
/// Upload to the App Store.
appStore('app-store', 'Upload to the App Store'),
/// Ad-hoc distribution.
adHoc(
'ad-hoc',
'''
Test on designated devices that do not need to be registered with the Apple developer account.
Requires a distribution certificate.''',
),
/// Development distribution.
development(
'development',
'''Test only on development devices registered with the Apple developer account.''',
),
/// Enterprise distribution.
enterprise(
'enterprise',
'Distribute an app registered with the Apple Developer Enterprise Program.',
@@ -81,6 +86,7 @@ final iosRef = create(Ios.new);
/// The [Ios] instance available in the current zone.
Ios get ios => read(iosRef);
/// A class that provides information about the iOS platform.
class Ios {
/// Returns the set of flavors for the iOS project, if the project has an
/// iOS platform configured.
@@ -1,5 +1,3 @@
// ignore_for_file: public_member_api_docs
import 'package:args/args.dart';
import 'package:collection/collection.dart';
import 'package:shorebird_code_push_client/shorebird_code_push_client.dart';
@@ -54,7 +52,9 @@ enum ReleaseType {
}
}
/// Extension on [ArgResults] to get the release types from the CLI arguments.
extension ReleaseTypeArgs on ArgResults {
/// The release types specified in the CLI arguments.
Iterable<ReleaseType> get releaseTypes {
List<String>? releaseTypeCliNames;
if (wasParsed('platforms')) {
@@ -1,5 +1,3 @@
// ignore_for_file: public_member_api_docs
import 'dart:io';
import 'package:collection/collection.dart';
@@ -33,14 +31,20 @@ class MultipleArtifactsFoundException implements Exception {
}
}
/// {@template artifact_not_found_exception}
/// Thrown when no artifact is found in the build directory.
/// {@endtemplate}
class ArtifactNotFoundException implements Exception {
ArtifactNotFoundException({
/// {@macro artifact_not_found_exception}
const ArtifactNotFoundException({
required this.artifactName,
required this.buildDir,
});
/// The name of the artifact.
final String artifactName;
/// The build directory where the artifact was expected to be.
final String buildDir;
@override
@@ -77,8 +81,10 @@ extension on String {
String get artifactId => replaceAll(RegExp(r'\W'), '').toLowerCase();
}
/// The reference to the [ShorebirdAndroidArtifacts] instance.
final shorebirdAndroidArtifactsRef = create(ShorebirdAndroidArtifacts.new);
/// The [ShorebirdAndroidArtifacts] instance available in the current zone.
ShorebirdAndroidArtifacts get shorebirdAndroidArtifacts =>
read(shorebirdAndroidArtifactsRef);
@@ -168,6 +174,7 @@ class ShorebirdAndroidArtifacts {
);
}
/// The path to the aar library.
static String get aarLibraryPath {
final projectRoot = shorebirdEnv.getShorebirdProjectRoot()!;
return p.joinAll([
@@ -179,6 +186,7 @@ class ShorebirdAndroidArtifacts {
]);
}
/// The path to the aar directory.
static String aarArtifactDirectory({
required String packageName,
required String buildNumber,
@@ -190,6 +198,7 @@ class ShorebirdAndroidArtifacts {
buildNumber,
]);
/// The path to the aar artifact.
static String aarArtifactPath({
required String packageName,
required String buildNumber,
@@ -1,3 +1,4 @@
// Allowing one member abstracts for consistency/namespace/ease of testing.
// ignore_for_file: one_member_abstracts
import 'dart:io';
@@ -1,5 +1,3 @@
// ignore_for_file: public_member_api_docs
import 'dart:async';
import 'package:args/args.dart';
@@ -19,14 +17,19 @@ import 'package:shorebird_cli/src/shorebird_version.dart';
import 'package:shorebird_cli/src/third_party/flutter_tools/lib/flutter_tools.dart';
import 'package:shorebird_cli/src/version.dart';
/// The name of the executable.
const executableName = 'shorebird';
/// The name of the package (e.g. name in the pubspec.yaml).
const packageName = 'shorebird_cli';
/// The package description.
const description = 'The shorebird command-line tool';
/// {@template shorebird_cli_command_runner}
/// A [CommandRunner] for the CLI.
///
/// ```
/// ```sh
/// $ shorebird --version
/// ```
/// {@endtemplate}
@@ -209,6 +212,10 @@ Engine • revision ${shorebirdEnv.shorebirdEngineRevision}''');
// When on an usage exception we don't need to show the "if you aren't
// sure" message, so we do an early return here.
return ExitCode.usage.code;
// We explicitly want to catch all exceptions here to log them and show
// the user a friendly message.
// ignore: avoid_catches_without_on_clauses
} catch (error, stackTrace) {
logger
..err('$error')
@@ -248,7 +255,7 @@ ${currentRunLogFile.absolute.path}
Future<String?> _tryGetFlutterVersion() async {
try {
return await shorebirdFlutter.getVersionString();
} catch (error) {
} on Exception catch (error) {
logger.detail('Unable to determine Flutter version.\n$error');
return null;
}
@@ -266,7 +273,7 @@ ${currentRunLogFile.absolute.path}
..info('A new version of shorebird is available!')
..info('Run ${lightCyan.wrap('shorebird upgrade')} to upgrade.');
}
} catch (error) {
} on Exception catch (error) {
logger.detail('Unable to check for updates.\n$error');
}
}
@@ -1,5 +1,3 @@
// ignore_for_file: public_member_api_docs
import 'dart:io';
import 'package:args/args.dart';
@@ -15,17 +13,22 @@ typedef HashFunction = String Function(List<int> bytes);
/// Signature for a function which takes a path to a zip file.
typedef UnzipFn = Future<void> Function(String zipFilePath, String outputDir);
/// Signature for a function which builds a [CodePushClient].
typedef CodePushClientBuilder = CodePushClient Function({
required http.Client httpClient,
Uri? hostedUri,
});
/// Signature for a function which starts a process (e.g. [Process.start]).
typedef StartProcess = Future<Process> Function(
String executable,
List<String> arguments, {
bool runInShell,
});
/// {@template shorebird_command}
/// A command in the Shorebird CLI.
/// {@endtemplate}
abstract class ShorebirdCommand extends Command<int> {
// We don't currently have a test involving both a CommandRunner
// and a Command, so we can't test this getter.
@@ -1,5 +1,3 @@
// ignore_for_file: public_member_api_docs
import 'dart:io' hide Platform;
import 'package:checked_yaml/checked_yaml.dart';
@@ -26,6 +24,7 @@ class ShorebirdEnv {
const ShorebirdEnv({String? flutterRevisionOverride})
: _flutterRevisionOverride = flutterRevisionOverride;
/// Copy the [ShorebirdEnv] and optionally override the flutter revision.
ShorebirdEnv copyWith({String? flutterRevisionOverride}) => ShorebirdEnv(
flutterRevisionOverride:
flutterRevisionOverride ?? _flutterRevisionOverride,
@@ -50,6 +49,7 @@ class ShorebirdEnv {
return File(platform.script.toFilePath()).parent.parent.parent;
}
/// The Shorebird engine revision.
String get shorebirdEngineRevision {
return File(
p.join(
@@ -61,6 +61,7 @@ class ShorebirdEnv {
).readAsStringSync().trim();
}
/// Set the Shorebird Flutter revision.
set flutterRevision(String revision) {
if (revision == flutterRevision) return;
File(
@@ -72,6 +73,7 @@ class ShorebirdEnv {
if (snapshot.existsSync()) snapshot.deleteSync();
}
/// Get the Shorebird Flutter revision.
String get flutterRevision {
return _flutterRevisionOverride ??
File(
@@ -176,10 +178,10 @@ class ShorebirdEnv {
Pubspec? getPubspecYaml() {
final root = getFlutterProjectRoot();
if (root == null) return null;
final yaml = getPubspecYamlFile(cwd: root).readAsStringSync();
try {
final yaml = getPubspecYamlFile(cwd: root).readAsStringSync();
return Pubspec.parse(yaml, lenient: true);
} catch (_) {
} on Exception {
return null;
}
}
@@ -217,7 +219,7 @@ class ShorebirdEnv {
final baseUrl = platform.environment['SHOREBIRD_HOSTED_URL'] ??
getShorebirdYaml()?.baseUrl;
return baseUrl == null ? null : Uri.tryParse(baseUrl);
} catch (_) {
} on Exception {
return null;
}
}
@@ -1,4 +1,3 @@
// ignore_for_file: public_member_api_docs
import 'dart:convert';
import 'dart:io';
@@ -26,7 +25,10 @@ class ShorebirdFlutter {
/// {@macro shorebird_flutter}
const ShorebirdFlutter();
/// The executable name.
static const executable = 'flutter';
/// The Shorebird Flutter fork git URL.
static const String flutterGitUrl =
'https://github.com/shorebirdtech/flutter.git';
@@ -41,6 +43,7 @@ class ShorebirdFlutter {
return p.join(shorebirdEnv.flutterDirectory.parent.path, revision);
}
/// Install the provided Flutter [revision].
Future<void> installRevision({required String revision}) async {
final targetDirectory = Directory(_workingDirectory(revision: revision));
if (targetDirectory.existsSync()) return;
@@ -85,7 +88,7 @@ class ShorebirdFlutter {
runInShell: true,
);
precacheProgress.complete();
} catch (_) {
} on Exception {
precacheProgress.fail('Failed to precache Flutter $version');
logger.info(
'''This is not a critical error, but your next build make take longer than usual.''',
@@ -149,7 +152,7 @@ class ShorebirdFlutter {
try {
version = await getVersionString();
} catch (_) {
} on Exception {
version = 'unknown';
}
@@ -223,7 +226,7 @@ class ShorebirdFlutter {
if (version != null) {
return versionOrHash;
}
} catch (_) {
} on Exception {
return null;
}
@@ -245,7 +248,7 @@ class ShorebirdFlutter {
final versionString =
await getVersionForRevision(flutterRevision: versionOrHash);
return versionString != null ? tryParseVersion(versionString) : null;
} catch (_) {
} on Exception {
return null;
}
}
@@ -260,6 +263,7 @@ class ShorebirdFlutter {
return LineSplitter.split(result).toList().firstOrNull;
}
/// Get the list of Flutter versions for the given [revision].
Future<List<String>> getVersions({String? revision}) async {
final result = await git.forEachRef(
format: '%(refname:short)',
@@ -271,6 +275,7 @@ class ShorebirdFlutter {
.toList();
}
/// Use the provided [version] of Flutter.
Future<void> useVersion({required String version}) async {
final revision = await git.revParse(
revision: 'origin/flutter_release/$version',
@@ -280,6 +285,7 @@ class ShorebirdFlutter {
await useRevision(revision: revision);
}
/// Use the provided [revision] of Flutter.
Future<void> useRevision({required String revision}) async {
await installRevision(revision: revision);
@@ -1,5 +1,3 @@
// ignore_for_file: public_member_api_docs
import 'package:collection/collection.dart';
import 'package:mason_logger/mason_logger.dart';
import 'package:scoped_deps/scoped_deps.dart';
@@ -9,25 +7,31 @@ import 'package:shorebird_cli/src/platform.dart';
import 'package:shorebird_cli/src/shorebird_env.dart';
import 'package:shorebird_cli/src/validators/validators.dart';
/// An exception thrown when a precondition for running a command is not met.
abstract interface class PreconditionFailedException implements Exception {
/// The exit code to use when the precondition fails.
ExitCode get exitCode;
}
/// An exception thrown when Shorebird has not been initialized.
class ShorebirdNotInitializedException implements PreconditionFailedException {
@override
ExitCode get exitCode => ExitCode.config;
}
/// An exception thrown when the user is not authorized to run a command.
class UserNotAuthorizedException implements PreconditionFailedException {
@override
ExitCode get exitCode => ExitCode.noUser;
}
/// An exception thrown when validation fails.
class ValidationFailedException implements PreconditionFailedException {
@override
ExitCode get exitCode => ExitCode.config;
}
/// An exception thrown when a command is run in an unsupported context.
class UnsupportedContextException implements PreconditionFailedException {
// coverage:ignore-start
@override
@@ -35,6 +39,7 @@ class UnsupportedContextException implements PreconditionFailedException {
// coverage:ignore-end
}
/// An exception thrown when the operating system is not supported.
class UnsupportedOperatingSystemException
implements PreconditionFailedException {
@override
@@ -54,7 +54,7 @@ class ShorebirdFlutterValidator extends Validator {
String? shorebirdFlutterVersionString;
try {
shorebirdFlutterVersionString = await _getFlutterVersion();
} catch (error) {
} on Exception catch (error) {
issues.add(
ValidationIssue.error(
message: 'Failed to determine Shorebird Flutter version. $error',
@@ -69,7 +69,7 @@ class ShorebirdFlutterValidator extends Validator {
);
} on CommandNotFoundException catch (_) {
// If there is no system Flutter, we don't throw a validation exception.
} catch (error) {
} on Exception catch (error) {
issues.add(
ValidationIssue.error(
message: 'Failed to determine path Flutter version. $error',
+2 -2
View File
@@ -799,10 +799,10 @@ packages:
dependency: "direct dev"
description:
name: very_good_analysis
sha256: "1fb637c0022034b1f19ea2acb42a3603cbd8314a470646a59a2fb01f5f3a8629"
sha256: "62d2b86d183fb81b2edc22913d9f155d26eb5cf3855173adb1f59fac85035c63"
url: "https://pub.dev"
source: hosted
version: "6.0.0"
version: "7.0.0"
vm_service:
dependency: transitive
description:
+1 -1
View File
@@ -55,7 +55,7 @@ dev_dependencies:
json_serializable: ^6.9.0
mocktail: ^1.0.3
test: ^1.25.9
very_good_analysis: ^6.0.0
very_good_analysis: ^7.0.0
executables:
shorebird:
@@ -331,7 +331,7 @@ Either run `flutter pub get` manually, or follow the steps in ${cannotRunInVSCod
flavor: any(named: 'flavor'),
),
).thenThrow(
ArtifactNotFoundException(
const ArtifactNotFoundException(
artifactName: 'app-release.aab',
buildDir: 'buildDir',
),
@@ -573,7 +573,7 @@ Either run `flutter pub get` manually, or follow the steps in ${cannotRunInVSCod
flavor: any(named: 'flavor'),
),
).thenThrow(
ArtifactNotFoundException(
const ArtifactNotFoundException(
artifactName: 'app-release.aab',
buildDir: 'buildDir',
),
@@ -209,6 +209,21 @@ void main() {
});
group('AuthenticatedClient', () {
group('isAuthenticated', () {
group('when credentials are malformed', () {
setUp(() {
File(
p.join(credentialsDir, 'credentials.json'),
).writeAsStringSync('invalid credentials');
auth = buildAuth();
});
test('returns false', () {
expect(auth.isAuthenticated, isFalse);
});
});
});
group('token', () {
test('does not require an onRefreshCredentials callback', () {
expect(
@@ -158,6 +158,7 @@ void main() {
late CodePushClient codePushClient;
late Ditto ditto;
late ShorebirdLogger logger;
late ShorebirdEnv shorebirdEnv;
late ShorebirdFlutter shorebirdFlutter;
late Progress progress;
late CodePushClientWrapper codePushClientWrapper;
@@ -171,6 +172,7 @@ void main() {
dittoRef.overrideWith(() => ditto),
loggerRef.overrideWith(() => logger),
platformRef.overrideWith(() => platform),
shorebirdEnvRef.overrideWith(() => shorebirdEnv),
shorebirdFlutterRef.overrideWith(() => shorebirdFlutter),
},
);
@@ -193,6 +195,7 @@ void main() {
() => CodePushClientWrapper(codePushClient: codePushClient),
);
shorebirdEnv = MockShorebirdEnv();
shorebirdFlutter = MockShorebirdFlutter();
when(
@@ -233,9 +236,7 @@ void main() {
displayName: appName,
organizationId: any(named: 'organizationId'),
),
).thenAnswer(
(_) async => app,
);
).thenAnswer((_) async => app);
await runWithOverrides(
() => codePushClientWrapper.createApp(
@@ -99,6 +99,7 @@ void main() {
when(
() => doctor.runValidators(any(), applyFixes: any(named: 'applyFixes')),
).thenAnswer((_) async => {});
when(shorebirdFlutter.getVersionString).thenAnswer((_) async => null);
command = runWithOverrides(DoctorCommand.new)
..testArgResults = argResults;
@@ -106,7 +107,11 @@ void main() {
test(
'prints shorebird version, flutter revision, '
'and engine revision', () async {
'and engine revision '
'when unable to determine Flutter version', () async {
when(
() => shorebirdFlutter.getVersionString(),
).thenThrow(Exception('oops'));
await runWithOverrides(command.run);
verify(
@@ -117,6 +122,11 @@ Flutter • revision ${shorebirdEnv.flutterRevision}
Engine revision $shorebirdEngineRevision
'''),
).called(1);
verify(
() => logger.detail(
'Unable to determine Flutter version.\nException: oops',
),
).called(1);
});
test(
@@ -46,7 +46,7 @@ void main() {
when(
() => shorebirdFlutter.getVersionString(),
).thenAnswer((_) async => '1.0.0');
when(() => shorebirdFlutter.getVersions()).thenThrow('error');
when(() => shorebirdFlutter.getVersions()).thenThrow(Exception('error'));
await expectLater(
runWithOverrides(command.run),
completion(equals(ExitCode.software.code)),
@@ -56,7 +56,7 @@ void main() {
() => shorebirdFlutter.getVersionString(),
() => shorebirdFlutter.getVersions(),
() => progress.fail('Failed to fetch Flutter versions.'),
() => logger.err('error'),
() => logger.err('Exception: error'),
]);
});
@@ -44,6 +44,15 @@ void main() {
when(() => results.wasParsed('provider')).thenReturn(false);
when(() => results['provider']).thenReturn(null);
when(() => auth.client).thenReturn(httpClient);
when(
() => auth.loginCI(any(), prompt: any(named: 'prompt')),
).thenAnswer(
(_) async => const CiToken(
// "shorebird-token" in base64
refreshToken: 'c2hvcmViaXJkLXRva2Vu', // cspell:disable-line
authProvider: AuthProvider.google,
),
);
when(
() => logger.chooseOne<AuthProvider>(
any(),
@@ -52,8 +61,9 @@ void main() {
),
).thenReturn(AuthProvider.google);
command =
runWithOverrides(() => LoginCiCommand()..testArgResults = results);
command = runWithOverrides(
() => LoginCiCommand()..testArgResults = results,
);
});
group('provider', () {
@@ -53,6 +53,10 @@ void main() {
when(() => auth.credentialsFilePath).thenReturn(
p.join(applicationConfigHome.path, 'credentials.json'),
);
when(
() => auth.login(any(), prompt: any(named: 'prompt')),
).thenAnswer((_) async {});
when(
() => logger.chooseOne<AuthProvider>(
any(),
@@ -61,8 +65,9 @@ void main() {
),
).thenReturn(AuthProvider.google);
command =
runWithOverrides(() => LoginCommand()..testArgResults = results);
command = runWithOverrides(
() => LoginCommand()..testArgResults = results,
);
});
group('provider', () {
@@ -440,7 +440,9 @@ void main() {
test('forwards --split-debug-info to builder', () async {
try {
await runWithOverrides(patcher.buildPatchArtifact);
} catch (_) {}
} on Exception {
// ignore
}
verify(
() => artifactBuilder.buildElfAotSnapshot(
appDillPath: any(named: 'appDillPath'),
@@ -643,6 +645,9 @@ void main() {
additionalArgs: any(named: 'additionalArgs'),
),
).thenAnswer((_) async => linkPercentage);
when(
aotTools.isGeneratePatchDiffBaseSupported,
).thenAnswer((_) async => false);
when(
() => shorebirdEnv.flutterRevision,
).thenReturn(postLinkerFlutterRevision);
@@ -735,7 +740,9 @@ void main() {
releaseArtifact: releaseArtifactFile,
),
);
} catch (_) {}
} on Exception {
// ignore
}
verify(
() => aotTools.link(
base: any(named: 'base'),
@@ -691,7 +691,9 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}''',
test('forwards --split-debug-info to builder', () async {
try {
await runWithOverrides(patcher.buildPatchArtifact);
} catch (_) {}
} on Exception {
// ignore
}
verify(
() => artifactBuilder.buildElfAotSnapshot(
appDillPath: any(named: 'appDillPath'),
@@ -1011,6 +1013,9 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}''',
additionalArgs: any(named: 'additionalArgs'),
),
).thenAnswer((_) async => linkPercentage);
when(
aotTools.isGeneratePatchDiffBaseSupported,
).thenAnswer((_) async => false);
when(
() => artifactManager.getIosAppDirectory(
xcarchiveDirectory: any(named: 'xcarchiveDirectory'),
@@ -1167,7 +1172,9 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}''',
releaseArtifact: releaseArtifactFile,
),
);
} catch (_) {}
} on Exception {
// ignore
}
verify(
() => aotTools.link(
base: any(named: 'base'),
@@ -1519,8 +1526,6 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}''',
kernel: any(named: 'kernel'),
outputPath: any(named: 'outputPath'),
workingDirectory: any(named: 'workingDirectory'),
// ignore: avoid_redundant_argument_values
dumpDebugInfoPath: null,
),
).called(1);
});
@@ -1691,7 +1696,9 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}''',
'Info.plist',
),
).deleteSync(recursive: true);
} catch (_) {}
} on Exception {
// ignore
}
});
test('exit with code 70', () async {
@@ -718,7 +718,9 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}''',
test('forwards --split-debug-info to builder', () async {
try {
await runWithOverrides(patcher.buildPatchArtifact);
} catch (_) {}
} on Exception {
// ignore
}
verify(
() => artifactBuilder.buildElfAotSnapshot(
appDillPath: any(named: 'appDillPath'),
@@ -1582,8 +1584,6 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}''',
kernel: any(named: 'kernel'),
outputPath: any(named: 'outputPath'),
workingDirectory: any(named: 'workingDirectory'),
// ignore: avoid_redundant_argument_values
dumpDebugInfoPath: null,
),
).called(1);
});
@@ -1621,7 +1621,9 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}''',
'Info.plist',
),
).deleteSync(recursive: true);
} catch (_) {}
} on Exception {
// ignore
}
});
test('exit with code 70', () async {
@@ -47,8 +47,8 @@ void main() {
() => shorebirdVersion.attemptReset(revision: any(named: 'revision')),
).thenAnswer((_) async => {});
when(() => progress.complete(any())).thenAnswer((_) {
final message = _.positionalArguments.elementAt(0) as String?;
when(() => progress.complete(any())).thenAnswer((invocation) {
final message = invocation.positionalArguments.elementAt(0) as String?;
if (message != null) progressLogs.add(message);
});
when(() => logger.progress(any())).thenReturn(progress);
@@ -83,6 +83,15 @@ void main() {
);
});
test('returns null if devicectl availability check throws', () async {
exitCode = ExitCode.software;
when(() => process.run(any(), any())).thenThrow(Exception('oops'));
await expectLater(
await runWithOverrides(() => devicectl.deviceForLaunch()),
isNull,
);
});
test('returns null if no CoreDevice with the given deviceID can be found',
() async {
exitCode = ExitCode.success;
@@ -47,7 +47,7 @@ void main() {
progress = MockProgress();
ioSink = MockIOSink();
shorebirdEnv = MockShorebirdEnv();
iosDeploy = IOSDeploy();
iosDeploy = const IOSDeploy();
final tempDir = Directory.systemTemp.createTempSync();
@@ -10,7 +10,7 @@ void main() {
});
test('returns null if string is of the format major.minor', () {
expect(tryParseVersion('1.2'), isNull);
expect(tryParseVersion('-1.2.-3'), isNull);
});
test('returns null if string is in an invalid format', () {
@@ -27,6 +27,13 @@ void main() {
expect(tryParseVersion('1.2', strict: false), Version(1, 2, 0));
});
test('returns null if string contains bigint', () {
expect(
tryParseVersion('999999999999999999999999999.0', strict: false),
isNull,
);
});
test('returns null if string is in an invalid format', () {
expect(tryParseVersion('asdf', strict: false), isNull);
});
@@ -41,6 +41,7 @@ void main() {
test('returns correct number of decimal places', () {
expect(formatBytes(1524, decimals: 0), equals('1 KB'));
expect(formatBytes(1524, decimals: 1), equals('1.5 KB'));
// Being explicit for test readability.
// ignore: avoid_redundant_argument_values
expect(formatBytes(1524, decimals: 2), equals('1.49 KB'));
expect(formatBytes(1524, decimals: 3), equals('1.488 KB'));
@@ -73,7 +73,7 @@ void main() {
group('when endpoints are not reachable', () {
setUp(() {
when(() => httpClient.send(any())).thenThrow(Exception('oops'));
when(() => httpClient.get(any())).thenThrow(Exception('oops'));
});
test('logs reachability for each checked url', () async {
@@ -355,13 +355,15 @@ Engine • revision $shorebirdEngineRevision''',
test('gracefully handles case when latest version cannot be determined',
() async {
when(shorebirdVersion.isLatest).thenThrow('error');
when(shorebirdVersion.isLatest).thenThrow(Exception('error'));
final result = await runWithOverrides(
() => commandRunner.run(['--version']),
);
expect(result, equals(ExitCode.success.code));
verify(
() => logger.detail('Unable to check for updates.\nerror'),
() => logger.detail(
'Unable to check for updates.\nException: error',
),
).called(1);
});
@@ -403,13 +405,15 @@ Engine • revision $shorebirdEngineRevision''',
test(
'gracefully handles case when flutter version cannot be determined',
() async {
when(shorebirdFlutter.getVersionString).thenThrow('error');
when(shorebirdFlutter.getVersionString).thenThrow(Exception('error'));
final result = await runWithOverrides(
() => commandRunner.run(['--version']),
);
expect(result, equals(ExitCode.success.code));
verify(
() => logger.detail('Unable to determine Flutter version.\nerror'),
() => logger.detail(
'Unable to determine Flutter version.\nException: error',
),
).called(1);
});
});
@@ -276,6 +276,28 @@ void main() {
);
});
test(
'returns null when error occurs reading pubspec.yaml',
() {
final tempDir = Directory.systemTemp.createTempSync();
final file = File(
p.join(tempDir.path, 'pubspec.yaml'),
)..writeAsStringSync('name: test');
// Make the file unreadable.
Process.runSync('chmod', ['000', file.path]);
expect(
IOOverrides.runZoned(
() => runWithOverrides(() => shorebirdEnv.getPubspecYaml()),
getCurrentDirectory: () => tempDir,
),
isNull,
);
},
onPlatform: {
'windows': const Skip('chmod is not available on Windows'),
},
);
test('returns value when pubspec.yaml exists', () {
final tempDir = Directory.systemTemp.createTempSync();
File(
@@ -639,6 +661,30 @@ base_url: https://example.com''');
test('returns null when there is no env override or shorebird.yaml', () {
expect(runWithOverrides(() => shorebirdEnv.hostedUri), isNull);
});
test(
'returns null when unable to read shorebird.yaml',
() {
final directory = Directory.systemTemp.createTempSync();
final file = File(p.join(directory.path, 'shorebird.yaml'))
..writeAsStringSync('''
app_id: test-id
base_url: https://example.com''');
// Make the file unreadable.
Process.runSync('chmod', ['000', file.path]);
expect(
IOOverrides.runZoned(
() => runWithOverrides(() => shorebirdEnv.hostedUri),
getCurrentDirectory: () => directory,
),
isNull,
);
},
onPlatform: {
'windows': const Skip('chmod is not available on Windows'),
},
);
});
group('canAcceptUserInput', () {
@@ -294,6 +294,26 @@ Tools • Dart 3.0.6 • DevTools 2.23.1''');
expect(revision, isNull);
});
});
group('when exception occurs doing revision lookup', () {
setUp(() {
when(
() => git.forEachRef(
directory: any(named: 'directory'),
contains: any(named: 'contains'),
format: any(named: 'format'),
pattern: any(named: 'pattern'),
),
).thenThrow(Exception('oops'));
});
test('returns null', () async {
final revision = await runWithOverrides(
() => shorebirdFlutter.resolveFlutterRevision('not-a-version'),
);
expect(revision, isNull);
});
});
});
group('resolveFlutterVersion', () {
@@ -327,6 +347,26 @@ Tools • Dart 3.0.6 • DevTools 2.23.1''');
});
});
group('when commit lookup fails', () {
setUp(() {
when(
() => git.forEachRef(
directory: any(named: 'directory'),
contains: any(named: 'contains'),
format: any(named: 'format'),
pattern: any(named: 'pattern'),
),
).thenThrow(Exception('oops'));
});
test('returns null', () async {
final revision = await runWithOverrides(
() => shorebirdFlutter.resolveFlutterVersion('not-a-version'),
);
expect(revision, isNull);
});
});
group('when input is a recognized commit hash', () {
setUp(() {
when(