From dbfdf2907356d3771d127686855c2732c2690eaf Mon Sep 17 00:00:00 2001 From: Bryan Oltman Date: Thu, 6 Apr 2023 17:58:06 -0400 Subject: [PATCH] feat: Set FLUTTER_STORAGE_BASE_URL when using Shorebird-vended Flutter, warn user when path flutter version does not match shorebird flutter version (#243) Co-authored-by: Eric Seidel Co-authored-by: Felix Angelov --- packages/shorebird_cli/lib/src/command.dart | 8 +- .../lib/src/commands/build_command.dart | 10 +- .../lib/src/commands/doctor_command.dart | 9 +- .../lib/src/commands/patch_command.dart | 5 + .../lib/src/commands/release_command.dart | 5 + .../lib/src/commands/run_command.dart | 6 +- .../shorebird_flutter_validator.dart | 70 --------- .../lib/src/doctor/validators/validators.dart | 3 - .../lib/src/flutter_validation_mixin.dart | 15 ++ ..._paths.dart => shorebird_environment.dart} | 5 +- .../lib/src/shorebird_process.dart | 74 +++++++++- ...android_internet_permission_validator.dart | 4 +- .../shorebird_flutter_validator.dart | 132 +++++++++++++++++ .../shorebird_version_validator.dart | 4 +- .../validators.dart} | 6 +- .../test/src/commands/build_command_test.dart | 42 ++++++ .../src/commands/doctor_command_test.dart | 3 +- .../test/src/commands/patch_command_test.dart | 55 +++++++ .../src/commands/release_command_test.dart | 55 +++++++ .../test/src/commands/run_command_test.dart | 53 +++++++ .../src/commands/upgrade_command_test.dart | 2 + .../test/src/shorebird_process_test.dart | 134 ++++++++++++++++++ ...id_internet_permission_validator_test.dart | 3 +- .../shorebird_flutter_validator_test.dart | 90 +++++++++++- .../shorebird_version_validator_test.dart | 5 +- 25 files changed, 694 insertions(+), 104 deletions(-) delete mode 100644 packages/shorebird_cli/lib/src/doctor/validators/shorebird_flutter_validator.dart delete mode 100644 packages/shorebird_cli/lib/src/doctor/validators/validators.dart create mode 100644 packages/shorebird_cli/lib/src/flutter_validation_mixin.dart rename packages/shorebird_cli/lib/src/{shorebird_paths.dart => shorebird_environment.dart} (83%) rename packages/shorebird_cli/lib/src/{doctor => }/validators/android_internet_permission_validator.dart (94%) create mode 100644 packages/shorebird_cli/lib/src/validators/shorebird_flutter_validator.dart rename packages/shorebird_cli/lib/src/{doctor => }/validators/shorebird_version_validator.dart (88%) rename packages/shorebird_cli/lib/src/{doctor/doctor_validator.dart => validators/validators.dart} (91%) rename packages/shorebird_cli/test/src/{commands/doctor => }/validators/android_internet_permission_validator_test.dart (96%) rename packages/shorebird_cli/test/src/{commands/doctor => }/validators/shorebird_flutter_validator_test.dart (52%) rename packages/shorebird_cli/test/src/{commands/doctor => }/validators/shorebird_version_validator_test.dart (94%) diff --git a/packages/shorebird_cli/lib/src/command.dart b/packages/shorebird_cli/lib/src/command.dart index ee96954a..53c7ca88 100644 --- a/packages/shorebird_cli/lib/src/command.dart +++ b/packages/shorebird_cli/lib/src/command.dart @@ -7,6 +7,7 @@ import 'package:mason_logger/mason_logger.dart'; import 'package:meta/meta.dart'; import 'package:shorebird_cli/src/auth/auth.dart'; import 'package:shorebird_cli/src/shorebird_process.dart'; +import 'package:shorebird_cli/src/validators/validators.dart'; import 'package:shorebird_code_push_client/shorebird_code_push_client.dart'; /// Signature for a function which takes a list of bytes and returns a hash. @@ -30,16 +31,21 @@ abstract class ShorebirdCommand extends Command { CodePushClientBuilder? buildCodePushClient, RunProcess? runProcess, StartProcess? startProcess, + ShorebirdFlutterValidator? flutterValidator, }) : auth = auth ?? Auth(), buildCodePushClient = buildCodePushClient ?? CodePushClient.new, runProcess = runProcess ?? ShorebirdProcess.run, - startProcess = startProcess ?? ShorebirdProcess.start; + startProcess = startProcess ?? ShorebirdProcess.start { + this.flutterValidator = flutterValidator ?? + ShorebirdFlutterValidator(runProcess: this.runProcess); + } final Auth auth; final CodePushClientBuilder buildCodePushClient; final Logger logger; final RunProcess runProcess; final StartProcess startProcess; + late final ShorebirdFlutterValidator flutterValidator; /// [ArgResults] used for testing purposes only. @visibleForTesting diff --git a/packages/shorebird_cli/lib/src/commands/build_command.dart b/packages/shorebird_cli/lib/src/commands/build_command.dart index 4d768796..a666b190 100644 --- a/packages/shorebird_cli/lib/src/commands/build_command.dart +++ b/packages/shorebird_cli/lib/src/commands/build_command.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:mason_logger/mason_logger.dart'; import 'package:shorebird_cli/src/command.dart'; +import 'package:shorebird_cli/src/flutter_validation_mixin.dart'; import 'package:shorebird_cli/src/shorebird_build_mixin.dart'; import 'package:shorebird_cli/src/shorebird_config_mixin.dart'; import 'package:shorebird_cli/src/shorebird_engine_mixin.dart'; @@ -12,13 +13,18 @@ import 'package:shorebird_cli/src/shorebird_engine_mixin.dart'; /// Build a new release of your application. /// {@endtemplate} class BuildCommand extends ShorebirdCommand - with ShorebirdConfigMixin, ShorebirdEngineMixin, ShorebirdBuildMixin { + with + FlutterValidationMixin, + ShorebirdConfigMixin, + ShorebirdEngineMixin, + ShorebirdBuildMixin { /// {@macro build_command} BuildCommand({ required super.logger, super.auth, super.buildCodePushClient, super.runProcess, + super.flutterValidator, }); @override @@ -43,6 +49,8 @@ class BuildCommand extends ShorebirdCommand return ExitCode.software.code; } + await logFlutterValidationIssues(); + final buildProgress = logger.progress('Building release '); try { await buildRelease(); diff --git a/packages/shorebird_cli/lib/src/commands/doctor_command.dart b/packages/shorebird_cli/lib/src/commands/doctor_command.dart index 415f3224..954e2f7c 100644 --- a/packages/shorebird_cli/lib/src/commands/doctor_command.dart +++ b/packages/shorebird_cli/lib/src/commands/doctor_command.dart @@ -1,8 +1,7 @@ import 'package:mason_logger/mason_logger.dart'; import 'package:shorebird_cli/src/command.dart'; -import 'package:shorebird_cli/src/doctor/doctor_validator.dart'; -import 'package:shorebird_cli/src/doctor/validators/validators.dart'; import 'package:shorebird_cli/src/shorebird_version_mixin.dart'; +import 'package:shorebird_cli/src/validators/validators.dart'; import 'package:shorebird_cli/src/version.dart'; /// {@template doctor_command} @@ -15,11 +14,11 @@ class DoctorCommand extends ShorebirdCommand with ShorebirdVersionMixin { /// {@macro doctor_command} DoctorCommand({ required super.logger, - List? validators, + List? validators, super.runProcess, }) { this.validators = validators ?? - [ + [ ShorebirdVersionValidator( isShorebirdVersionCurrent: isShorebirdVersionCurrent, ), @@ -28,7 +27,7 @@ class DoctorCommand extends ShorebirdCommand with ShorebirdVersionMixin { ]; } - late final List validators; + late final List validators; @override String get name => 'doctor'; diff --git a/packages/shorebird_cli/lib/src/commands/patch_command.dart b/packages/shorebird_cli/lib/src/commands/patch_command.dart index dc0aedf3..a63b31ba 100644 --- a/packages/shorebird_cli/lib/src/commands/patch_command.dart +++ b/packages/shorebird_cli/lib/src/commands/patch_command.dart @@ -6,6 +6,7 @@ import 'package:http/http.dart' as http; import 'package:mason_logger/mason_logger.dart'; import 'package:path/path.dart' as p; import 'package:shorebird_cli/src/command.dart'; +import 'package:shorebird_cli/src/flutter_validation_mixin.dart'; import 'package:shorebird_cli/src/shorebird_build_mixin.dart'; import 'package:shorebird_cli/src/shorebird_config_mixin.dart'; import 'package:shorebird_cli/src/shorebird_create_app_mixin.dart'; @@ -18,6 +19,7 @@ import 'package:shorebird_code_push_client/shorebird_code_push_client.dart'; /// {@endtemplate} class PatchCommand extends ShorebirdCommand with + FlutterValidationMixin, ShorebirdConfigMixin, ShorebirdEngineMixin, ShorebirdBuildMixin, @@ -28,6 +30,7 @@ class PatchCommand extends ShorebirdCommand super.auth, super.buildCodePushClient, super.runProcess, + super.flutterValidator, HashFunction? hashFn, http.Client? httpClient, }) : _hashFn = hashFn ?? ((m) => sha256.convert(m).toString()), @@ -105,6 +108,8 @@ class PatchCommand extends ShorebirdCommand return ExitCode.software.code; } + await logFlutterValidationIssues(); + final force = results['force'] == true; final dryRun = results['dry-run'] == true; diff --git a/packages/shorebird_cli/lib/src/commands/release_command.dart b/packages/shorebird_cli/lib/src/commands/release_command.dart index b7af976f..35f120af 100644 --- a/packages/shorebird_cli/lib/src/commands/release_command.dart +++ b/packages/shorebird_cli/lib/src/commands/release_command.dart @@ -5,6 +5,7 @@ import 'package:crypto/crypto.dart'; import 'package:mason_logger/mason_logger.dart'; import 'package:path/path.dart' as p; import 'package:shorebird_cli/src/command.dart'; +import 'package:shorebird_cli/src/flutter_validation_mixin.dart'; import 'package:shorebird_cli/src/shorebird_build_mixin.dart'; import 'package:shorebird_cli/src/shorebird_config_mixin.dart'; import 'package:shorebird_cli/src/shorebird_create_app_mixin.dart'; @@ -17,6 +18,7 @@ import 'package:shorebird_code_push_client/shorebird_code_push_client.dart'; /// {@endtemplate} class ReleaseCommand extends ShorebirdCommand with + FlutterValidationMixin, ShorebirdConfigMixin, ShorebirdEngineMixin, ShorebirdBuildMixin, @@ -27,6 +29,7 @@ class ReleaseCommand extends ShorebirdCommand super.auth, super.buildCodePushClient, super.runProcess, + super.flutterValidator, HashFunction? hashFn, }) : _hashFn = hashFn ?? ((m) => sha256.convert(m).toString()) { argParser @@ -83,6 +86,8 @@ make smaller updates to your app. return ExitCode.software.code; } + await logFlutterValidationIssues(); + final buildProgress = logger.progress('Building release'); try { await buildRelease(); diff --git a/packages/shorebird_cli/lib/src/commands/run_command.dart b/packages/shorebird_cli/lib/src/commands/run_command.dart index 562a4259..22829eea 100644 --- a/packages/shorebird_cli/lib/src/commands/run_command.dart +++ b/packages/shorebird_cli/lib/src/commands/run_command.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:mason_logger/mason_logger.dart'; import 'package:shorebird_cli/src/command.dart'; +import 'package:shorebird_cli/src/flutter_validation_mixin.dart'; import 'package:shorebird_cli/src/shorebird_config_mixin.dart'; import 'package:shorebird_cli/src/shorebird_engine_mixin.dart'; @@ -10,13 +11,14 @@ import 'package:shorebird_cli/src/shorebird_engine_mixin.dart'; /// Run the Flutter application. /// {@endtemplate} class RunCommand extends ShorebirdCommand - with ShorebirdConfigMixin, ShorebirdEngineMixin { + with FlutterValidationMixin, ShorebirdConfigMixin, ShorebirdEngineMixin { /// {@macro run_command} RunCommand({ required super.logger, super.auth, super.buildCodePushClient, super.startProcess, + super.flutterValidator, }); @override @@ -41,6 +43,8 @@ class RunCommand extends ShorebirdCommand return ExitCode.software.code; } + await logFlutterValidationIssues(); + logger.info('Running app...'); final process = await startProcess( 'flutter', diff --git a/packages/shorebird_cli/lib/src/doctor/validators/shorebird_flutter_validator.dart b/packages/shorebird_cli/lib/src/doctor/validators/shorebird_flutter_validator.dart deleted file mode 100644 index a69e07d4..00000000 --- a/packages/shorebird_cli/lib/src/doctor/validators/shorebird_flutter_validator.dart +++ /dev/null @@ -1,70 +0,0 @@ -import 'package:shorebird_cli/src/doctor/doctor_validator.dart'; -import 'package:shorebird_cli/src/shorebird_paths.dart'; -import 'package:shorebird_cli/src/shorebird_process.dart'; - -class ShorebirdFlutterValidator extends DoctorValidator { - ShorebirdFlutterValidator({required this.runProcess}); - - final RunProcess runProcess; - - // coverage:ignore-start - @override - String get description => 'Flutter install is correct'; - // coverage:ignore-end - - @override - Future> validate() async { - if (!ShorebirdPaths.flutterDirectory.existsSync()) { - final message = - 'No Flutter directory found at ${ShorebirdPaths.flutterDirectory}'; - return [ - ValidationIssue( - severity: ValidationIssueSeverity.error, - message: message, - ), - ]; - } - - if (!await _flutterDirectoryIsClean()) { - return [ - ValidationIssue( - severity: ValidationIssueSeverity.warning, - message: '${ShorebirdPaths.flutterDirectory} has local modifications', - ), - ]; - } - - if (!await _flutterDirectoryTracksStable()) { - final message = - '${ShorebirdPaths.flutterDirectory} is not on the "stable" branch'; - return [ - ValidationIssue( - severity: ValidationIssueSeverity.warning, - message: message, - ), - ]; - } - - return []; - } - - Future _flutterDirectoryIsClean() async { - final result = await runProcess( - 'git', - ['status'], - workingDirectory: ShorebirdPaths.flutterDirectory.path, - ); - return result.stdout - .toString() - .contains('nothing to commit, working tree clean'); - } - - Future _flutterDirectoryTracksStable() async { - final result = await runProcess( - 'git', - ['--no-pager', 'branch'], - workingDirectory: ShorebirdPaths.flutterDirectory.path, - ); - return result.stdout.toString().contains('* stable'); - } -} diff --git a/packages/shorebird_cli/lib/src/doctor/validators/validators.dart b/packages/shorebird_cli/lib/src/doctor/validators/validators.dart deleted file mode 100644 index ef136e40..00000000 --- a/packages/shorebird_cli/lib/src/doctor/validators/validators.dart +++ /dev/null @@ -1,3 +0,0 @@ -export 'android_internet_permission_validator.dart'; -export 'shorebird_flutter_validator.dart'; -export 'shorebird_version_validator.dart'; diff --git a/packages/shorebird_cli/lib/src/flutter_validation_mixin.dart b/packages/shorebird_cli/lib/src/flutter_validation_mixin.dart new file mode 100644 index 00000000..6084bde1 --- /dev/null +++ b/packages/shorebird_cli/lib/src/flutter_validation_mixin.dart @@ -0,0 +1,15 @@ +import 'package:shorebird_cli/src/command.dart'; +import 'package:shorebird_cli/src/validators/shorebird_flutter_validator.dart'; + +mixin FlutterValidationMixin on ShorebirdCommand { + /// Runs [ShorebirdFlutterValidator.validate] and writes validation issues to + /// stdout. + Future logFlutterValidationIssues() async { + final flutterValidationIssues = await flutterValidator.validate(); + if (flutterValidationIssues.isNotEmpty) { + for (final issue in flutterValidationIssues) { + logger.info(issue.displayMessage); + } + } + } +} diff --git a/packages/shorebird_cli/lib/src/shorebird_paths.dart b/packages/shorebird_cli/lib/src/shorebird_environment.dart similarity index 83% rename from packages/shorebird_cli/lib/src/shorebird_paths.dart rename to packages/shorebird_cli/lib/src/shorebird_environment.dart index d0a5ef1c..f335b7a4 100644 --- a/packages/shorebird_cli/lib/src/shorebird_paths.dart +++ b/packages/shorebird_cli/lib/src/shorebird_environment.dart @@ -4,10 +4,13 @@ import 'package:meta/meta.dart'; import 'package:path/path.dart' as p; import 'package:platform/platform.dart'; -abstract class ShorebirdPaths { +abstract class ShorebirdEnvironment { @visibleForTesting static Platform platform = const LocalPlatform(); + /// Environment variables from [Platform.environment]. + static Map get environment => platform.environment; + /// The root directory of the Shorebird install. /// /// Assumes we are running from $ROOT/bin/cache. diff --git a/packages/shorebird_cli/lib/src/shorebird_process.dart b/packages/shorebird_cli/lib/src/shorebird_process.dart index 2f76d378..aa106140 100644 --- a/packages/shorebird_cli/lib/src/shorebird_process.dart +++ b/packages/shorebird_cli/lib/src/shorebird_process.dart @@ -1,19 +1,23 @@ import 'dart:io'; import 'package:meta/meta.dart'; -import 'package:shorebird_cli/src/shorebird_paths.dart'; +import 'package:shorebird_cli/src/shorebird_environment.dart'; typedef RunProcess = Future Function( String executable, List arguments, { bool runInShell, + Map? environment, String? workingDirectory, + bool useVendedFlutter, }); typedef StartProcess = Future Function( String executable, List arguments, { bool runInShell, + Map? environment, + bool useVendedFlutter, }); /// A wrapper around [Process] that replaces executables to Shorebird-vended @@ -26,42 +30,100 @@ abstract class ShorebirdProcess { String executable, List arguments, { bool runInShell = false, + Map? environment, String? workingDirectory, + bool useVendedFlutter = true, }) { + final resolvedEnvironment = environment ?? {}; + if (useVendedFlutter) { + // Note: this will overwrite existing environment values. + resolvedEnvironment.addAll( + _environmentOverrides(executable: executable), + ); + } + return processWrapper.run( - _resolveExecutable(executable), + useVendedFlutter ? _resolveExecutable(executable) : executable, arguments, runInShell: runInShell, workingDirectory: workingDirectory, + environment: resolvedEnvironment, ); } static Future start( String executable, List argument, { + Map? environment, bool runInShell = false, + bool useVendedFlutter = true, }) { + final resolvedEnvironment = environment ?? {}; + if (useVendedFlutter) { + // Note: this will overwrite existing environment values. + resolvedEnvironment.addAll( + _environmentOverrides(executable: executable), + ); + } + return processWrapper.start( - _resolveExecutable(executable), + useVendedFlutter ? _resolveExecutable(executable) : executable, argument, runInShell: runInShell, + environment: resolvedEnvironment, ); } static String _resolveExecutable(String executable) { if (executable == 'flutter') { - return ShorebirdPaths.flutterBinaryFile.path; + return ShorebirdEnvironment.flutterBinaryFile.path; } return executable; } + + static Map _environmentOverrides({ + required String executable, + }) { + if (executable == 'flutter') { + return {'FLUTTER_STORAGE_BASE_URL': 'https://download.shorebird.dev/'}; + } + + return {}; + } } // coverage:ignore-start @visibleForTesting class ProcessWrapper { - RunProcess get run => Process.run; + RunProcess get run => ( + String executable, + List arguments, { + bool runInShell = false, + Map? environment, + String? workingDirectory, + bool useVendedFlutter = true, + }) => + Process.run( + executable, + arguments, + environment: environment, + runInShell: runInShell, + workingDirectory: workingDirectory, + ); - StartProcess get start => Process.start; + StartProcess get start => ( + String executable, + List arguments, { + bool runInShell = false, + Map? environment, + bool useVendedFlutter = true, + }) => + Process.start( + executable, + arguments, + runInShell: runInShell, + environment: environment, + ); } // coverage:ignore-end diff --git a/packages/shorebird_cli/lib/src/doctor/validators/android_internet_permission_validator.dart b/packages/shorebird_cli/lib/src/validators/android_internet_permission_validator.dart similarity index 94% rename from packages/shorebird_cli/lib/src/doctor/validators/android_internet_permission_validator.dart rename to packages/shorebird_cli/lib/src/validators/android_internet_permission_validator.dart index 4cf2a213..4d26de62 100644 --- a/packages/shorebird_cli/lib/src/doctor/validators/android_internet_permission_validator.dart +++ b/packages/shorebird_cli/lib/src/validators/android_internet_permission_validator.dart @@ -1,14 +1,14 @@ import 'dart:io'; import 'package:path/path.dart' as p; -import 'package:shorebird_cli/src/doctor/doctor_validator.dart'; +import 'package:shorebird_cli/src/validators/validators.dart'; import 'package:xml/xml.dart'; /// Checks that all AndroidManifest.xml files in android/app/src/{flavor}/ /// contain the INTERNET permission, which is required for Shorebird to work. /// /// See https://github.com/shorebirdtech/shorebird/issues/160. -class AndroidInternetPermissionValidator extends DoctorValidator { +class AndroidInternetPermissionValidator extends Validator { // coverage:ignore-start @override String get description => diff --git a/packages/shorebird_cli/lib/src/validators/shorebird_flutter_validator.dart b/packages/shorebird_cli/lib/src/validators/shorebird_flutter_validator.dart new file mode 100644 index 00000000..0c07fe72 --- /dev/null +++ b/packages/shorebird_cli/lib/src/validators/shorebird_flutter_validator.dart @@ -0,0 +1,132 @@ +import 'package:shorebird_cli/src/shorebird_environment.dart'; +import 'package:shorebird_cli/src/shorebird_process.dart'; +import 'package:shorebird_cli/src/validators/validators.dart'; + +class ShorebirdFlutterValidator extends Validator { + ShorebirdFlutterValidator({required this.runProcess}); + + final RunProcess runProcess; + final _flutterVersionRegex = RegExp(r'Flutter (\d+.\d+.\d+)'); + + // coverage:ignore-start + @override + String get description => 'Flutter install is correct'; + // coverage:ignore-end + + @override + Future> validate() async { + final issues = []; + + if (!ShorebirdEnvironment.flutterDirectory.existsSync()) { + final message = 'No Flutter directory found at ' + '${ShorebirdEnvironment.flutterDirectory}'; + issues.add( + ValidationIssue( + severity: ValidationIssueSeverity.error, + message: message, + ), + ); + } + + if (!await _flutterDirectoryIsClean()) { + issues.add( + ValidationIssue( + severity: ValidationIssueSeverity.warning, + message: '${ShorebirdEnvironment.flutterDirectory} has local ' + 'modifications', + ), + ); + } + + if (!await _flutterDirectoryTracksStable()) { + final message = + '${ShorebirdEnvironment.flutterDirectory} is not on the "stable" ' + 'branch'; + issues.add( + ValidationIssue( + severity: ValidationIssueSeverity.warning, + message: message, + ), + ); + } + + final shorebirdFlutterVersion = await _shorebirdFlutterVersion(); + final pathFlutterVersion = await _pathFlutterVersion(); + + if (shorebirdFlutterVersion != pathFlutterVersion) { + final message = """ +Shorebird Flutter and the Flutter on your path are different versions. +\tShorebird Flutter: $shorebirdFlutterVersion +\tSystem Flutter: $pathFlutterVersion''' +This can cause unexpected behavior if the version gap is wide. If you're seeing this unexpectedly, please let us know on Shorebird discord!"""; + + issues.add( + ValidationIssue( + severity: ValidationIssueSeverity.warning, + message: message, + ), + ); + } + + final flutterStorageEnvironmentValue = + ShorebirdEnvironment.environment['FLUTTER_STORAGE_BASE_URL']; + if (flutterStorageEnvironmentValue != null && + flutterStorageEnvironmentValue.isNotEmpty) { + issues.add( + const ValidationIssue( + severity: ValidationIssueSeverity.warning, + message: 'Shorebird does not respect the FLUTTER_STORAGE_BASE_URL ' + 'environment variable at this time', + ), + ); + } + + return issues; + } + + Future _flutterDirectoryIsClean() async { + final result = await runProcess( + 'git', + ['status'], + workingDirectory: ShorebirdEnvironment.flutterDirectory.path, + ); + return result.stdout + .toString() + .contains('nothing to commit, working tree clean'); + } + + Future _flutterDirectoryTracksStable() async { + final result = await runProcess( + 'git', + ['--no-pager', 'branch'], + workingDirectory: ShorebirdEnvironment.flutterDirectory.path, + ); + return result.stdout.toString().contains('* stable'); + } + + Future _shorebirdFlutterVersion() => _getFlutterVersion( + checkPathFlutter: false, + ); + + Future _pathFlutterVersion() => _getFlutterVersion( + checkPathFlutter: true, + ); + + Future _getFlutterVersion({ + required bool checkPathFlutter, + }) async { + final result = await runProcess( + 'flutter', + ['--version'], + useVendedFlutter: !checkPathFlutter, + ); + final output = result.stdout.toString(); + + final match = _flutterVersionRegex.firstMatch(output); + if (match == null) { + throw Exception('Could not find version match in $output'); + } + + return match.group(1)!; + } +} diff --git a/packages/shorebird_cli/lib/src/doctor/validators/shorebird_version_validator.dart b/packages/shorebird_cli/lib/src/validators/shorebird_version_validator.dart similarity index 88% rename from packages/shorebird_cli/lib/src/doctor/validators/shorebird_version_validator.dart rename to packages/shorebird_cli/lib/src/validators/shorebird_version_validator.dart index bf9e10eb..636d402a 100644 --- a/packages/shorebird_cli/lib/src/doctor/validators/shorebird_version_validator.dart +++ b/packages/shorebird_cli/lib/src/validators/shorebird_version_validator.dart @@ -1,10 +1,10 @@ import 'dart:io'; import 'package:path/path.dart' as p; -import 'package:shorebird_cli/src/doctor/doctor_validator.dart'; +import 'package:shorebird_cli/src/validators/validators.dart'; /// Verifies that the currently installed version of Shorebird is the latest. -class ShorebirdVersionValidator extends DoctorValidator { +class ShorebirdVersionValidator extends Validator { ShorebirdVersionValidator({required this.isShorebirdVersionCurrent}); final Future Function({required String workingDirectory}) diff --git a/packages/shorebird_cli/lib/src/doctor/doctor_validator.dart b/packages/shorebird_cli/lib/src/validators/validators.dart similarity index 91% rename from packages/shorebird_cli/lib/src/doctor/doctor_validator.dart rename to packages/shorebird_cli/lib/src/validators/validators.dart index 9e199735..238b0110 100644 --- a/packages/shorebird_cli/lib/src/doctor/doctor_validator.dart +++ b/packages/shorebird_cli/lib/src/validators/validators.dart @@ -1,6 +1,10 @@ import 'package:mason_logger/mason_logger.dart'; import 'package:meta/meta.dart'; +export 'android_internet_permission_validator.dart'; +export 'shorebird_flutter_validator.dart'; +export 'shorebird_version_validator.dart'; + /// Severity level of a [ValidationIssue]. /// /// [error]s should be fixed before continuing development. @@ -60,7 +64,7 @@ class ValidationIssue { /// Checks for a specific issue with either the Shorebird installation or the /// current Shorebird project. -abstract class DoctorValidator { +abstract class Validator { /// A one-sentence explanation of what this validator is checking. String get description; diff --git a/packages/shorebird_cli/test/src/commands/build_command_test.dart b/packages/shorebird_cli/test/src/commands/build_command_test.dart index bf0a3f8d..2b389783 100644 --- a/packages/shorebird_cli/test/src/commands/build_command_test.dart +++ b/packages/shorebird_cli/test/src/commands/build_command_test.dart @@ -9,6 +9,7 @@ import 'package:mocktail/mocktail.dart'; import 'package:shorebird_cli/src/auth/auth.dart'; import 'package:shorebird_cli/src/commands/build_command.dart'; import 'package:shorebird_cli/src/config/config.dart'; +import 'package:shorebird_cli/src/validators/validators.dart'; import 'package:shorebird_code_push_client/shorebird_code_push_client.dart'; import 'package:test/test.dart'; @@ -26,6 +27,9 @@ class _MockProcessResult extends Mock implements ProcessResult {} class _MockCodePushClient extends Mock implements CodePushClient {} +class _MockShorebirdFlutterValidator extends Mock + implements ShorebirdFlutterValidator {} + void main() { group('build', () { late ArgResults argResults; @@ -36,6 +40,7 @@ void main() { late Logger logger; late ProcessResult processResult; late BuildCommand buildCommand; + late ShorebirdFlutterValidator flutterValidator; setUp(() { applicationConfigHome = Directory.systemTemp.createTempSync(); @@ -45,6 +50,7 @@ void main() { codePushClient = _MockCodePushClient(); logger = _MockLogger(); processResult = _MockProcessResult(); + flutterValidator = _MockShorebirdFlutterValidator(); buildCommand = BuildCommand( auth: auth, buildCodePushClient: ({ @@ -58,10 +64,13 @@ void main() { executable, arguments, { bool runInShell = false, + Map? environment, String? workingDirectory, + bool useVendedFlutter = true, }) async { return processResult; }, + flutterValidator: flutterValidator, )..testArgResults = argResults; testApplicationConfigHome = (_) => applicationConfigHome.path; @@ -72,6 +81,8 @@ void main() { () => codePushClient.downloadEngine(revision: any(named: 'revision')), ).thenAnswer((_) async => Uint8List.fromList([])); when(() => logger.progress(any())).thenReturn(_MockProgress()); + when(() => logger.info(any())).thenReturn(null); + when(() => flutterValidator.validate()).thenAnswer((_) async => []); }); test('exits with no user when not logged in', () async { @@ -130,5 +141,36 @@ void main() { expect(result, equals(ExitCode.success.code)); }); + + test('prints flutter validation warnings', () async { + when(() => flutterValidator.validate()).thenAnswer( + (_) async => [ + const ValidationIssue( + severity: ValidationIssueSeverity.warning, + message: 'Flutter issue 1', + ), + const ValidationIssue( + severity: ValidationIssueSeverity.warning, + message: 'Flutter issue 2', + ), + ], + ); + when(() => processResult.exitCode).thenReturn(ExitCode.success.code); + when( + () => codePushClient.downloadEngine(revision: any(named: 'revision')), + ).thenAnswer( + (_) async => Uint8List.fromList(ZipEncoder().encode(Archive())!), + ); + + final result = await buildCommand.run(); + + expect(result, equals(ExitCode.success.code)); + verify( + () => logger.info(any(that: contains('Flutter issue 1'))), + ).called(1); + verify( + () => logger.info(any(that: contains('Flutter issue 2'))), + ).called(1); + }); }); } diff --git a/packages/shorebird_cli/test/src/commands/doctor_command_test.dart b/packages/shorebird_cli/test/src/commands/doctor_command_test.dart index 2d926d62..4f677a7c 100644 --- a/packages/shorebird_cli/test/src/commands/doctor_command_test.dart +++ b/packages/shorebird_cli/test/src/commands/doctor_command_test.dart @@ -1,8 +1,7 @@ import 'package:mason_logger/mason_logger.dart'; import 'package:mocktail/mocktail.dart'; import 'package:shorebird_cli/src/commands/commands.dart'; -import 'package:shorebird_cli/src/doctor/doctor_validator.dart'; -import 'package:shorebird_cli/src/doctor/validators/validators.dart'; +import 'package:shorebird_cli/src/validators/validators.dart'; import 'package:test/test.dart'; class _MockShorebirdVersionValidator extends Mock diff --git a/packages/shorebird_cli/test/src/commands/patch_command_test.dart b/packages/shorebird_cli/test/src/commands/patch_command_test.dart index d99c0a62..189976d8 100644 --- a/packages/shorebird_cli/test/src/commands/patch_command_test.dart +++ b/packages/shorebird_cli/test/src/commands/patch_command_test.dart @@ -10,6 +10,7 @@ import 'package:path/path.dart' as p; import 'package:shorebird_cli/src/auth/auth.dart'; import 'package:shorebird_cli/src/commands/patch_command.dart'; import 'package:shorebird_cli/src/config/config.dart'; +import 'package:shorebird_cli/src/validators/validators.dart'; import 'package:shorebird_code_push_client/shorebird_code_push_client.dart'; import 'package:test/test.dart'; @@ -29,6 +30,9 @@ class _MockHttpClient extends Mock implements http.Client {} class _MockCodePushClient extends Mock implements CodePushClient {} +class _MockShorebirdFlutterValidator extends Mock + implements ShorebirdFlutterValidator {} + void main() { group('patch', () { const appId = 'test-app-id'; @@ -85,6 +89,7 @@ flutter: late CodePushClient codePushClient; late PatchCommand command; late Uri? capturedHostedUri; + late ShorebirdFlutterValidator flutterValidator; Directory setUpTempDir() { final tempDir = Directory.systemTemp.createTempSync(); @@ -111,6 +116,7 @@ flutter: patchProcessResult = _MockProcessResult(); httpClient = _MockHttpClient(); codePushClient = _MockCodePushClient(); + flutterValidator = _MockShorebirdFlutterValidator(); command = PatchCommand( auth: auth, buildCodePushClient: ({ @@ -124,7 +130,9 @@ flutter: executable, arguments, { bool runInShell = false, + Map? environment, String? workingDirectory, + bool useVendedFlutter = true, }) async { if (executable == 'flutter') return flutterBuildProcessResult; if (executable.endsWith('patch')) return patchProcessResult; @@ -132,6 +140,7 @@ flutter: }, logger: logger, httpClient: httpClient, + flutterValidator: flutterValidator, )..testArgResults = argResults; testApplicationConfigHome = (_) => applicationConfigHome.path; @@ -198,6 +207,7 @@ flutter: channelId: any(named: 'channelId'), ), ).thenAnswer((_) async {}); + when(() => flutterValidator.validate()).thenAnswer((_) async => []); }); test('throws config error when shorebird is not initialized', () async { @@ -838,5 +848,50 @@ base_url: $baseUrl''', ); expect(capturedHostedUri, equals(Uri.parse(baseUrl))); }); + + test('prints flutter validation warnings', () async { + final tempDir = setUpTempDir(); + Directory( + p.join(command.shorebirdEnginePath, 'engine'), + ).createSync(recursive: true); + final artifactPath = p.join( + tempDir.path, + 'build', + 'app', + 'intermediates', + 'stripped_native_libs', + 'release', + 'out', + 'lib', + 'arm64-v8a', + 'libapp.so', + ); + File(artifactPath).createSync(recursive: true); + when(() => flutterValidator.validate()).thenAnswer( + (_) async => [ + const ValidationIssue( + severity: ValidationIssueSeverity.warning, + message: 'Flutter issue 1', + ), + const ValidationIssue( + severity: ValidationIssueSeverity.warning, + message: 'Flutter issue 2', + ), + ], + ); + + final exitCode = await IOOverrides.runZoned( + command.run, + getCurrentDirectory: () => tempDir, + ); + + expect(exitCode, equals(ExitCode.success.code)); + verify( + () => logger.info(any(that: contains('Flutter issue 1'))), + ).called(1); + verify( + () => logger.info(any(that: contains('Flutter issue 2'))), + ).called(1); + }); }); } diff --git a/packages/shorebird_cli/test/src/commands/release_command_test.dart b/packages/shorebird_cli/test/src/commands/release_command_test.dart index 062c2b75..acc94bdf 100644 --- a/packages/shorebird_cli/test/src/commands/release_command_test.dart +++ b/packages/shorebird_cli/test/src/commands/release_command_test.dart @@ -10,6 +10,7 @@ import 'package:path/path.dart' as p; import 'package:shorebird_cli/src/auth/auth.dart'; import 'package:shorebird_cli/src/commands/commands.dart'; import 'package:shorebird_cli/src/config/config.dart'; +import 'package:shorebird_cli/src/validators/validators.dart'; import 'package:shorebird_code_push_client/shorebird_code_push_client.dart'; import 'package:test/test.dart'; @@ -27,6 +28,9 @@ class _MockProcessResult extends Mock implements ProcessResult {} class _MockCodePushClient extends Mock implements CodePushClient {} +class _MockShorebirdFlutterValidator extends Mock + implements ShorebirdFlutterValidator {} + void main() { group('release', () { const appId = 'test-app-id'; @@ -71,6 +75,7 @@ flutter: late CodePushClient codePushClient; late ReleaseCommand command; late Uri? capturedHostedUri; + late ShorebirdFlutterValidator flutterValidator; Directory setUpTempDir() { final tempDir = Directory.systemTemp.createTempSync(); @@ -92,6 +97,7 @@ flutter: logger = _MockLogger(); processResult = _MockProcessResult(); codePushClient = _MockCodePushClient(); + flutterValidator = _MockShorebirdFlutterValidator(); command = ReleaseCommand( auth: auth, buildCodePushClient: ({ @@ -105,11 +111,14 @@ flutter: executable, arguments, { bool runInShell = false, + Map? environment, String? workingDirectory, + bool useVendedFlutter = true, }) async { return processResult; }, logger: logger, + flutterValidator: flutterValidator, )..testArgResults = argResults; testApplicationConfigHome = (_) => applicationConfigHome.path; @@ -148,6 +157,7 @@ flutter: hash: any(named: 'hash'), ), ).thenAnswer((_) async => releaseArtifact); + when(() => flutterValidator.validate()).thenAnswer((_) async => []); }); test('throws config error when shorebird is not initialized', () async { @@ -446,5 +456,50 @@ Did you forget to run "shorebird init"?''', expect(exitCode, ExitCode.success.code); expect(capturedHostedUri, isNull); }); + + test('prints flutter validation warnings', () async { + when(() => flutterValidator.validate()).thenAnswer( + (_) async => [ + const ValidationIssue( + severity: ValidationIssueSeverity.warning, + message: 'Flutter issue 1', + ), + const ValidationIssue( + severity: ValidationIssueSeverity.warning, + message: 'Flutter issue 2', + ), + ], + ); + final tempDir = setUpTempDir(); + Directory( + p.join(command.shorebirdEnginePath, 'engine'), + ).createSync(recursive: true); + final artifactPath = p.join( + tempDir.path, + 'build', + 'app', + 'intermediates', + 'stripped_native_libs', + 'release', + 'out', + 'lib', + 'arm64-v8a', + 'libapp.so', + ); + File(artifactPath).createSync(recursive: true); + final exitCode = await IOOverrides.runZoned( + command.run, + getCurrentDirectory: () => tempDir, + ); + verify(() => logger.success('\n✅ Published Release!')).called(1); + expect(exitCode, ExitCode.success.code); + expect(capturedHostedUri, isNull); + verify( + () => logger.info(any(that: contains('Flutter issue 1'))), + ).called(1); + verify( + () => logger.info(any(that: contains('Flutter issue 2'))), + ).called(1); + }); }); } diff --git a/packages/shorebird_cli/test/src/commands/run_command_test.dart b/packages/shorebird_cli/test/src/commands/run_command_test.dart index 099436ea..b80a165b 100644 --- a/packages/shorebird_cli/test/src/commands/run_command_test.dart +++ b/packages/shorebird_cli/test/src/commands/run_command_test.dart @@ -12,6 +12,7 @@ import 'package:path/path.dart' as p; import 'package:shorebird_cli/src/auth/auth.dart'; import 'package:shorebird_cli/src/commands/run_command.dart'; import 'package:shorebird_cli/src/config/config.dart'; +import 'package:shorebird_cli/src/validators/validators.dart'; import 'package:shorebird_code_push_client/shorebird_code_push_client.dart'; import 'package:test/test.dart'; @@ -29,6 +30,9 @@ class _MockProcess extends Mock implements Process {} class _MockCodePushClient extends Mock implements CodePushClient {} +class _MockShorebirdFlutterValidator extends Mock + implements ShorebirdFlutterValidator {} + void main() { group('run', () { late ArgResults argResults; @@ -39,6 +43,7 @@ void main() { late Process process; late CodePushClient codePushClient; late RunCommand runCommand; + late ShorebirdFlutterValidator flutterValidator; setUp(() { argResults = _MockArgResults(); @@ -48,6 +53,7 @@ void main() { logger = _MockLogger(); process = _MockProcess(); codePushClient = _MockCodePushClient(); + flutterValidator = _MockShorebirdFlutterValidator(); runCommand = RunCommand( auth: auth, logger: logger, @@ -60,6 +66,7 @@ void main() { startProcess: (executable, arguments, {bool runInShell = false}) async { return process; }, + flutterValidator: flutterValidator, )..testArgResults = argResults; testApplicationConfigHome = (_) => applicationConfigHome.path; @@ -68,6 +75,7 @@ void main() { when(() => auth.isAuthenticated).thenReturn(true); when(() => auth.client).thenReturn(httpClient); when(() => logger.progress(any())).thenReturn(_MockProgress()); + when(() => flutterValidator.validate()).thenAnswer((_) async => []); }); test('exits with no user when not logged in', () async { @@ -184,5 +192,50 @@ void main() { await expectLater(result, equals(ExitCode.success.code)); verify(() => logger.info(output)).called(1); }); + + test('prints flutter validation warnings', () async { + when(() => flutterValidator.validate()).thenAnswer( + (_) async => [ + const ValidationIssue( + severity: ValidationIssueSeverity.warning, + message: 'Flutter issue 1', + ), + const ValidationIssue( + severity: ValidationIssueSeverity.warning, + message: 'Flutter issue 2', + ), + ], + ); + final tempDir = Directory.systemTemp.createTempSync(); + Directory( + p.join(runCommand.shorebirdEnginePath, 'engine'), + ).createSync(recursive: true); + + final progress = _MockProgress(); + when(() => logger.progress(any())).thenReturn(progress); + + const output = 'some output'; + when( + () => process.stdout, + ).thenAnswer((_) => Stream.value(utf8.encode(output))); + when(() => process.stderr).thenAnswer((_) => const Stream.empty()); + when( + () => process.exitCode, + ).thenAnswer((_) async => ExitCode.success.code); + + final result = await IOOverrides.runZoned( + () => runCommand.run(), + getCurrentDirectory: () => tempDir, + ); + + await expectLater(result, equals(ExitCode.success.code)); + verify(() => logger.info(output)).called(1); + verify( + () => logger.info(any(that: contains('Flutter issue 1'))), + ).called(1); + verify( + () => logger.info(any(that: contains('Flutter issue 2'))), + ).called(1); + }); }); } diff --git a/packages/shorebird_cli/test/src/commands/upgrade_command_test.dart b/packages/shorebird_cli/test/src/commands/upgrade_command_test.dart index ed3335a7..de1eff27 100644 --- a/packages/shorebird_cli/test/src/commands/upgrade_command_test.dart +++ b/packages/shorebird_cli/test/src/commands/upgrade_command_test.dart @@ -38,7 +38,9 @@ void main() { executable, arguments, { bool runInShell = false, + Map? environment, workingDirectory, + bool useVendedFlutter = true, }) async { if (executable == 'git') { const revParseHead = ['rev-parse', '--verify', 'HEAD']; diff --git a/packages/shorebird_cli/test/src/shorebird_process_test.dart b/packages/shorebird_cli/test/src/shorebird_process_test.dart index 77b8d851..fc80d223 100644 --- a/packages/shorebird_cli/test/src/shorebird_process_test.dart +++ b/packages/shorebird_cli/test/src/shorebird_process_test.dart @@ -23,12 +23,15 @@ void main() { ShorebirdProcess.processWrapper = processWrapper; + // TODO(bryanoltman): this method of mocking processWrappper is not correctly matching arguments when(() => processWrapper.run).thenReturn( ( executable, arguments, { bool runInShell = false, + Map? environment, String? workingDirectory, + bool useVendedFlutter = true, }) async { return runProcessResult; }, @@ -39,6 +42,8 @@ void main() { executable, arguments, { bool runInShell = false, + Map? environment, + bool useVendedFlutter = true, }) async { return startProcess; }, @@ -81,6 +86,75 @@ void main() { ), ).called(1); }); + + test( + 'does not replace flutter with our local flutter if' + ' useVendedFlutter is false', () async { + await ShorebirdProcess.run( + 'flutter', + ['--version'], + runInShell: true, + workingDirectory: '~', + useVendedFlutter: false, + ); + + verify( + () => processWrapper.run( + 'flutter', + ['--version'], + runInShell: true, + workingDirectory: '~', + ), + ).called(1); + }); + + test('Updates environment if useVendedFlutter is true', () async { + await ShorebirdProcess.run( + 'flutter', + ['--version'], + runInShell: true, + workingDirectory: '~', + useVendedFlutter: false, + environment: {'ENV_VAR': 'asdfasdf'}, + ); + + verify( + () => processWrapper.run( + 'flutter', + ['--version'], + runInShell: true, + workingDirectory: '~', + environment: { + 'ENV_VAR': 'asdfasdf', + 'FLUTTER_STORAGE_BASE_URL': 'https://download.shorebird.dev/', + }, + ), + ).called(1); + }); + + test( + 'Makes no changes to environment if useVendedFlutter is false', + () async { + await ShorebirdProcess.run( + 'flutter', + ['--version'], + runInShell: true, + workingDirectory: '~', + useVendedFlutter: false, + environment: {'ENV_VAR': 'asdfasdf'}, + ); + + verify( + () => processWrapper.run( + 'flutter', + ['--version'], + runInShell: true, + workingDirectory: '~', + environment: {'ENV_VAR': 'asdfasdf'}, + ), + ).called(1); + }, + ); }); group('start', () { @@ -102,6 +176,66 @@ void main() { ), ).called(1); }); + + test( + 'does not replace flutter with our local flutter if' + ' useVendedFlutter is false', () async { + await ShorebirdProcess.start( + 'flutter', + ['--version'], + runInShell: true, + useVendedFlutter: false, + ); + + verify( + () => processWrapper.start( + 'flutter', + ['--version'], + runInShell: true, + ), + ).called(1); + }); }); + + test('Updates environment if useVendedFlutter is true', () async { + await ShorebirdProcess.start( + 'flutter', + ['--version'], + runInShell: true, + ); + + verify( + () => processWrapper.start( + 'flutter', + ['--version'], + runInShell: true, + environment: { + 'ENV_VAR': 'asdfasdf', + 'FLUTTER_STORAGE_BASE_URL': 'https://download.shorebird.dev/', + }, + ), + ).called(1); + }); + + test( + 'Makes no changes to environment if useVendedFlutter is false', + () async { + await ShorebirdProcess.start( + 'flutter', + ['--version'], + runInShell: true, + useVendedFlutter: false, + ); + + verify( + () => processWrapper.start( + 'flutter', + ['--version'], + runInShell: true, + environment: null, + ), + ).called(1); + }, + ); }); } diff --git a/packages/shorebird_cli/test/src/commands/doctor/validators/android_internet_permission_validator_test.dart b/packages/shorebird_cli/test/src/validators/android_internet_permission_validator_test.dart similarity index 96% rename from packages/shorebird_cli/test/src/commands/doctor/validators/android_internet_permission_validator_test.dart rename to packages/shorebird_cli/test/src/validators/android_internet_permission_validator_test.dart index dab7765c..3581762e 100644 --- a/packages/shorebird_cli/test/src/commands/doctor/validators/android_internet_permission_validator_test.dart +++ b/packages/shorebird_cli/test/src/validators/android_internet_permission_validator_test.dart @@ -2,8 +2,7 @@ import 'dart:io'; import 'package:collection/collection.dart'; import 'package:path/path.dart' as p; -import 'package:shorebird_cli/src/doctor/doctor_validator.dart'; -import 'package:shorebird_cli/src/doctor/validators/validators.dart'; +import 'package:shorebird_cli/src/validators/validators.dart'; import 'package:test/test.dart'; void main() { diff --git a/packages/shorebird_cli/test/src/commands/doctor/validators/shorebird_flutter_validator_test.dart b/packages/shorebird_cli/test/src/validators/shorebird_flutter_validator_test.dart similarity index 52% rename from packages/shorebird_cli/test/src/commands/doctor/validators/shorebird_flutter_validator_test.dart rename to packages/shorebird_cli/test/src/validators/shorebird_flutter_validator_test.dart index 43497aad..03018d3f 100644 --- a/packages/shorebird_cli/test/src/commands/doctor/validators/shorebird_flutter_validator_test.dart +++ b/packages/shorebird_cli/test/src/validators/shorebird_flutter_validator_test.dart @@ -4,9 +4,8 @@ import 'package:collection/collection.dart'; import 'package:mocktail/mocktail.dart'; import 'package:path/path.dart' as p; import 'package:platform/platform.dart'; -import 'package:shorebird_cli/src/doctor/doctor_validator.dart'; -import 'package:shorebird_cli/src/doctor/validators/validators.dart'; -import 'package:shorebird_cli/src/shorebird_paths.dart'; +import 'package:shorebird_cli/src/shorebird_environment.dart'; +import 'package:shorebird_cli/src/validators/validators.dart'; import 'package:test/test.dart'; class _MockProcessResult extends Mock implements ProcessResult {} @@ -25,10 +24,26 @@ nothing to commit, working tree clean const gitBranchMessage = ''' main * stable +'''; + + const pathFlutterVersionMessage = ''' +Flutter 3.7.9 • channel unknown • unknown source +Framework • revision 62bd79521d (7 days ago) • 2023-03-30 10:59:36 -0700 +Engine • revision ec975089ac +Tools • Dart 2.19.6 • DevTools 2.20.1 +'''; + + const shorebirdFlutterVersionMessage = ''' +Flutter 3.7.9 • channel stable • https://github.com/shorebirdtech/flutter.git +Framework • revision 62bd79521d (7 days ago) • 2023-03-30 10:59:36 -0700 +Engine • revision ec975089ac +Tools • Dart 2.19.6 • DevTools 2.20.1 '''; late ShorebirdFlutterValidator validator; late Directory tempDir; + late ProcessResult pathFlutterVersionProcessResult; + late ProcessResult shorebirdFlutterVersionProcessResult; late ProcessResult gitBranchProcessResult; late ProcessResult gitStatusProcessResult; @@ -48,10 +63,13 @@ nothing to commit, working tree clean setUp(() { tempDir = setupTempDirectory(); - ShorebirdPaths.platform = _MockPlatform(); - when(() => ShorebirdPaths.platform.script) + ShorebirdEnvironment.platform = _MockPlatform(); + when(() => ShorebirdEnvironment.platform.script) .thenReturn(shorebirdScriptFile(tempDir).uri); + when(() => ShorebirdEnvironment.platform.environment).thenReturn({}); + pathFlutterVersionProcessResult = _MockProcessResult(); + shorebirdFlutterVersionProcessResult = _MockProcessResult(); gitBranchProcessResult = _MockProcessResult(); gitStatusProcessResult = _MockProcessResult(); @@ -60,7 +78,9 @@ nothing to commit, working tree clean executable, arguments, { bool runInShell = false, + Map? environment, workingDirectory, + bool useVendedFlutter = true, }) async { if (executable == 'git') { if (arguments.equals(['status'])) { @@ -68,11 +88,23 @@ nothing to commit, working tree clean } else if (arguments.equals(['--no-pager', 'branch'])) { return gitBranchProcessResult; } + } else if (executable == 'flutter') { + if (arguments.equals(['--version'])) { + if (useVendedFlutter) { + return shorebirdFlutterVersionProcessResult; + } else { + return pathFlutterVersionProcessResult; + } + } } return _MockProcessResult(); }, ); + when(() => pathFlutterVersionProcessResult.stdout) + .thenReturn(pathFlutterVersionMessage); + when(() => shorebirdFlutterVersionProcessResult.stdout) + .thenReturn(shorebirdFlutterVersionMessage); when(() => gitBranchProcessResult.stdout).thenReturn(gitBranchMessage); when(() => gitStatusProcessResult.stdout).thenReturn(gitStatusMessage); }); @@ -116,5 +148,53 @@ nothing to commit, working tree clean expect(results.first.severity, ValidationIssueSeverity.warning); expect(results.first.message, contains('is not on the "stable" branch')); }); + + test( + 'warns when path flutter version does not match shorebird flutter' + ' version', + () async { + when(() => pathFlutterVersionProcessResult.stdout).thenReturn( + pathFlutterVersionMessage.replaceAll('3.7.9', '3.7.10'), + ); + + final results = await validator.validate(); + + expect(results, hasLength(1)); + expect(results.first.severity, ValidationIssueSeverity.warning); + expect( + results.first.message, + contains('Shorebird Flutter and the Flutter on your path are' + ' different versions'), + ); + }, + ); + + test( + 'warns if FLUTTER_STORAGE_BASE_URL has a non-empty value', + () async { + when(() => ShorebirdEnvironment.platform.environment).thenReturn( + {'FLUTTER_STORAGE_BASE_URL': 'https://storage.flutter-io.cn'}, + ); + + final results = await validator.validate(); + + expect(results, hasLength(1)); + expect(results.first.severity, ValidationIssueSeverity.warning); + expect( + results.first.message, + contains( + 'Shorebird does not respect the FLUTTER_STORAGE_BASE_URL ' + 'environment variable', + ), + ); + }, + ); + + test('throws exception if flutter version output is malformed', () async { + when(() => pathFlutterVersionProcessResult.stdout) + .thenReturn('OH NO THERE IS NO FLUTTER VERSION HERE'); + + expect(() async => validator.validate(), throwsException); + }); }); } diff --git a/packages/shorebird_cli/test/src/commands/doctor/validators/shorebird_version_validator_test.dart b/packages/shorebird_cli/test/src/validators/shorebird_version_validator_test.dart similarity index 94% rename from packages/shorebird_cli/test/src/commands/doctor/validators/shorebird_version_validator_test.dart rename to packages/shorebird_cli/test/src/validators/shorebird_version_validator_test.dart index 396ec4f1..9ab91dab 100644 --- a/packages/shorebird_cli/test/src/commands/doctor/validators/shorebird_version_validator_test.dart +++ b/packages/shorebird_cli/test/src/validators/shorebird_version_validator_test.dart @@ -3,8 +3,7 @@ import 'dart:io'; import 'package:mason_logger/mason_logger.dart'; import 'package:mocktail/mocktail.dart'; import 'package:shorebird_cli/src/commands/doctor_command.dart'; -import 'package:shorebird_cli/src/doctor/doctor_validator.dart'; -import 'package:shorebird_cli/src/doctor/validators/shorebird_version_validator.dart'; +import 'package:shorebird_cli/src/validators/validators.dart'; import 'package:test/test.dart'; class _MockLogger extends Mock implements Logger {} @@ -33,7 +32,9 @@ void main() { executable, arguments, { bool runInShell = false, + Map? environment, workingDirectory, + bool useVendedFlutter = true, }) async { if (executable == 'git') { const revParseHead = ['rev-parse', '--verify', 'HEAD'];