diff --git a/README.md b/README.md index bece7bfa..ffc9fd72 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,8 @@ https://github.com/shorebirdtech/old_repo ## Getting Started -Refer to [shorebird/install](https://github.com/shorebirdtech/install) for installation instructions. +Refer to [shorebird/install](https://github.com/shorebirdtech/install) for +installation instructions. ## Packages @@ -47,17 +48,24 @@ We currently assume the Dart from the Flutter SDK on the 'stable' channel. Due to the way the Dart compiler works, Shorebird requires an exact version of Flutter/Dart to operate correctly today. -We currently assume Rust 1.67.0 or later, although the code is unlikely to be -sensitive to the exact version of Rust. - -Once both are installed, `./scripts/bootstrap.sh` will run `pub get` -and `cargo check` for all packages in the repository. +Once both are installed, `./scripts/bootstrap.sh` will run `pub get` all +packages in the repository. ### Running tests We don't yet have a script to run tests locally. For now, you can run tests -manually by running `cargo test` in a Rust package directory or `dart test` in -a Dart package directory. +manually by running `dart test` in a Dart package directory. + +### Tracking coverage + +The following command will generate a coverage report for the Dart packages: + +```bash +dart test --coverage=coverage && dart pub global run coverage:format_coverage --lcov --in=coverage --out=coverage/lcov.info --packages=.dart_tool/package_config.json --check-ignore +``` + +We don't yet have a recommended way to view the coverage report but there are +several extensions available in VSCode. ## License diff --git a/packages/shorebird_cli/lib/src/command.dart b/packages/shorebird_cli/lib/src/command.dart index 36f03c18..00b10891 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/cache.dart'; +import 'package:shorebird_cli/src/command_runner.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'; @@ -25,12 +26,10 @@ typedef StartProcess = Future Function( bool runInShell, }); -List _defaultValidators({required RunProcess runProcess}) { - return [ - ShorebirdFlutterValidator(runProcess: runProcess), - AndroidInternetPermissionValidator(), - ]; -} +List _defaultValidators() => [ + ShorebirdFlutterValidator(), + AndroidInternetPermissionValidator(), + ]; abstract class ShorebirdCommand extends Command { ShorebirdCommand({ @@ -38,24 +37,42 @@ abstract class ShorebirdCommand extends Command { Auth? auth, Cache? cache, CodePushClientBuilder? buildCodePushClient, - RunProcess? runProcess, - StartProcess? startProcess, - List? validators, + List? validators, // For mocking. }) : auth = auth ?? Auth(), cache = cache ?? Cache(), buildCodePushClient = buildCodePushClient ?? CodePushClient.new, - runProcess = runProcess ?? ShorebirdProcess.run, - startProcess = startProcess ?? ShorebirdProcess.start { - this.validators = - validators ?? _defaultValidators(runProcess: this.runProcess); - } + validators = validators ?? _defaultValidators(); final Auth auth; final Cache cache; final CodePushClientBuilder buildCodePushClient; final Logger logger; - final RunProcess runProcess; - final StartProcess startProcess; + + // We don't currently have a test involving both a CommandRunner + // and a Command, so we can't test this getter. + // coverage:ignore-start + @override + ShorebirdCliCommandRunner? get runner => + super.runner as ShorebirdCliCommandRunner?; + // coverage:ignore-end + + /// [ShorebirdProcess] used for testing purposes only. + @visibleForTesting + ShorebirdProcess? testProcess; + + // If you hit a late initialization error here, it's because you're either + // using process before runCommand has been called, or you're in a test + // and should set testProcess instead. + ShorebirdProcess get process => testProcess ?? runner!.process; + + /// [EngineConfig] used for testing purposes only. + @visibleForTesting + EngineConfig? testEngineConfig; + + // If you hit a late initialization error here, it's because you're either + // using engineConfig before runCommand has been called, or you're in a test + // and should set testEngineConfig instead. + EngineConfig get engineConfig => testEngineConfig ?? process.engineConfig; /// Checks that the Shorebird install and project are in a good state. late List validators; diff --git a/packages/shorebird_cli/lib/src/command_runner.dart b/packages/shorebird_cli/lib/src/command_runner.dart index 2fd8ddd3..6c294459 100644 --- a/packages/shorebird_cli/lib/src/command_runner.dart +++ b/packages/shorebird_cli/lib/src/command_runner.dart @@ -4,6 +4,7 @@ import 'package:cli_completion/cli_completion.dart'; import 'package:mason_logger/mason_logger.dart'; import 'package:shorebird_cli/src/commands/commands.dart'; import 'package:shorebird_cli/src/shorebird_environment.dart'; +import 'package:shorebird_cli/src/shorebird_process.dart'; import 'package:shorebird_cli/src/version.dart'; const executableName = 'shorebird'; @@ -33,6 +34,18 @@ class ShorebirdCliCommandRunner extends CompletionCommandRunner { ..addFlag( 'verbose', help: 'Noisy logging, including all shell commands executed.', + ) + ..addOption( + 'local-engine-src-path', + hide: true, + help: 'Path to your engine src directory, if you are building Flutter ' + 'locally.', + ) + ..addOption( + 'local-engine', + hide: true, + help: 'Name of a build output within the engine out directory, if you ' + 'are building Flutter locally.', ); addCommand(AccountCommand(logger: _logger)); @@ -55,6 +68,9 @@ class ShorebirdCliCommandRunner extends CompletionCommandRunner { void printUsage() => _logger.info(usage); final Logger _logger; + // Currently using ShorebirdCliCommandRunner as our context object. + late final ShorebirdProcess process; + late final EngineConfig engineConfig; @override Future run(Iterable args) async { @@ -64,6 +80,15 @@ class ShorebirdCliCommandRunner extends CompletionCommandRunner { _logger.level = Level.verbose; } + // Set up our context before running the command. + engineConfig = EngineConfig( + localEngineSrcPath: topLevelResults['local-engine-src-path'] as String?, + localEngine: topLevelResults['local-engine'] as String?, + ); + process = ShorebirdProcess( + engineConfig: engineConfig, + ); + return await runCommand(topLevelResults) ?? ExitCode.success.code; } on FormatException catch (e, stackTrace) { // On format errors, show the commands error message, root usage and diff --git a/packages/shorebird_cli/lib/src/commands/build/build_apk_command.dart b/packages/shorebird_cli/lib/src/commands/build/build_apk_command.dart index ddbaef08..ce131feb 100644 --- a/packages/shorebird_cli/lib/src/commands/build/build_apk_command.dart +++ b/packages/shorebird_cli/lib/src/commands/build/build_apk_command.dart @@ -17,7 +17,6 @@ class BuildApkCommand extends ShorebirdCommand BuildApkCommand({ required super.logger, super.auth, - super.runProcess, super.validators, }); diff --git a/packages/shorebird_cli/lib/src/commands/build/build_app_bundle_command.dart.dart b/packages/shorebird_cli/lib/src/commands/build/build_app_bundle_command.dart.dart index 40126c4d..3cc6c435 100644 --- a/packages/shorebird_cli/lib/src/commands/build/build_app_bundle_command.dart.dart +++ b/packages/shorebird_cli/lib/src/commands/build/build_app_bundle_command.dart.dart @@ -17,7 +17,6 @@ class BuildAppBundleCommand extends ShorebirdCommand BuildAppBundleCommand({ required super.logger, super.auth, - super.runProcess, super.validators, }); diff --git a/packages/shorebird_cli/lib/src/commands/build/build_command.dart b/packages/shorebird_cli/lib/src/commands/build/build_command.dart index fd0a8b69..37cab547 100644 --- a/packages/shorebird_cli/lib/src/commands/build/build_command.dart +++ b/packages/shorebird_cli/lib/src/commands/build/build_command.dart @@ -17,7 +17,6 @@ class BuildCommand extends ShorebirdCommand BuildApkCommand( auth: auth, logger: logger, - runProcess: runProcess, validators: validators, ), ); @@ -25,7 +24,6 @@ class BuildCommand extends ShorebirdCommand BuildAppBundleCommand( auth: auth, logger: logger, - runProcess: runProcess, validators: validators, ), ); diff --git a/packages/shorebird_cli/lib/src/commands/doctor_command.dart b/packages/shorebird_cli/lib/src/commands/doctor_command.dart index 35485f78..0f164c81 100644 --- a/packages/shorebird_cli/lib/src/commands/doctor_command.dart +++ b/packages/shorebird_cli/lib/src/commands/doctor_command.dart @@ -17,7 +17,6 @@ class DoctorCommand extends ShorebirdCommand with ShorebirdVersionMixin { DoctorCommand({ required super.logger, super.validators, - super.runProcess, }) { validators = _allValidators(baseValidators: validators); } @@ -26,7 +25,7 @@ class DoctorCommand extends ShorebirdCommand with ShorebirdVersionMixin { ShorebirdVersionValidator( isShorebirdVersionCurrent: isShorebirdVersionCurrent, ), - ShorebirdFlutterValidator(runProcess: runProcess), + ShorebirdFlutterValidator(), AndroidInternetPermissionValidator(), ]; @@ -46,7 +45,7 @@ Shorebird Engine • revision ${ShorebirdEnvironment.shorebirdEngineRevision}''' var numIssues = 0; for (final validator in validators) { final progress = logger.progress(validator.description); - final issues = await validator.validate(); + final issues = await validator.validate(process); numIssues += issues.length; if (issues.isEmpty) { progress.complete(); diff --git a/packages/shorebird_cli/lib/src/commands/patch_command.dart b/packages/shorebird_cli/lib/src/commands/patch_command.dart index 01614536..b02f875d 100644 --- a/packages/shorebird_cli/lib/src/commands/patch_command.dart +++ b/packages/shorebird_cli/lib/src/commands/patch_command.dart @@ -46,7 +46,6 @@ class PatchCommand extends ShorebirdCommand super.auth, super.buildCodePushClient, super.cache, - super.runProcess, super.validators, HashFunction? hashFn, http.Client? httpClient, @@ -215,7 +214,7 @@ Please create a release using "shorebird release" and try again. final fetchReleaseArtifactProgress = logger.progress( 'Fetching release artifacts', ); - for (final entry in ShorebirdBuildMixin.architectures.entries) { + for (final entry in architectures.entries) { try { final releaseArtifact = await codePushClient.getReleaseArtifact( releaseId: release.id, @@ -251,8 +250,7 @@ Please create a release using "shorebird release" and try again. final createDiffProgress = logger.progress('Creating artifacts'); for (final releaseArtifactPath in releaseArtifactPaths.entries) { - final archMetadata = - ShorebirdBuildMixin.architectures[releaseArtifactPath.key]!; + final archMetadata = architectures[releaseArtifactPath.key]!; final patchArtifactPath = p.join( Directory.current.path, 'build', @@ -413,7 +411,7 @@ ${styleBold.wrap(lightGreen.wrap('🚀 Ready to publish a new patch!'))} diffPath, ]; - final result = await runProcess( + final result = await process.run( diffExecutable, diffArguments, runInShell: true, diff --git a/packages/shorebird_cli/lib/src/commands/release_command.dart b/packages/shorebird_cli/lib/src/commands/release_command.dart index e350e9b7..6fb09215 100644 --- a/packages/shorebird_cli/lib/src/commands/release_command.dart +++ b/packages/shorebird_cli/lib/src/commands/release_command.dart @@ -26,7 +26,6 @@ class ReleaseCommand extends ShorebirdCommand required super.logger, super.auth, super.buildCodePushClient, - super.runProcess, super.validators, HashFunction? hashFn, }) : _hashFn = hashFn ?? ((m) => sha256.convert(m).toString()) { @@ -126,7 +125,7 @@ Did you forget to run "shorebird init"?''', ); final platform = results['platform'] as String; - final archNames = ShorebirdBuildMixin.architectures.keys.map( + final archNames = architectures.keys.map( (arch) => arch.name, ); @@ -172,7 +171,7 @@ ${styleBold.wrap(lightGreen.wrap('🚀 Ready to create a new release!'))} } final createArtifactProgress = logger.progress('Creating artifacts'); - for (final archMetadata in ShorebirdBuildMixin.architectures.values) { + for (final archMetadata in architectures.values) { final artifactPath = p.join( Directory.current.path, 'build', diff --git a/packages/shorebird_cli/lib/src/commands/run_command.dart b/packages/shorebird_cli/lib/src/commands/run_command.dart index fb3711f5..ee5d4587 100644 --- a/packages/shorebird_cli/lib/src/commands/run_command.dart +++ b/packages/shorebird_cli/lib/src/commands/run_command.dart @@ -16,7 +16,6 @@ class RunCommand extends ShorebirdCommand required super.logger, super.auth, super.buildCodePushClient, - super.startProcess, super.validators, }); @@ -38,7 +37,7 @@ class RunCommand extends ShorebirdCommand await logValidationIssues(); logger.info('Running app...'); - final process = await startProcess( + final flutter = await process.start( 'flutter', [ 'run', @@ -49,13 +48,13 @@ class RunCommand extends ShorebirdCommand runInShell: true, ); - process.stdout.listen((event) { + flutter.stdout.listen((event) { logger.info(utf8.decode(event)); }); - process.stderr.listen((event) { + flutter.stderr.listen((event) { logger.err(utf8.decode(event)); }); - return process.exitCode; + return flutter.exitCode; } } diff --git a/packages/shorebird_cli/lib/src/commands/upgrade_command.dart b/packages/shorebird_cli/lib/src/commands/upgrade_command.dart index 106ad45d..f1b10cf2 100644 --- a/packages/shorebird_cli/lib/src/commands/upgrade_command.dart +++ b/packages/shorebird_cli/lib/src/commands/upgrade_command.dart @@ -11,7 +11,7 @@ import 'package:shorebird_cli/src/shorebird_version_mixin.dart'; /// {@endtemplate} class UpgradeCommand extends ShorebirdCommand with ShorebirdVersionMixin { /// {@macro upgrade_command} - UpgradeCommand({required super.logger, super.runProcess}); + UpgradeCommand({required super.logger}); @override String get description => 'Upgrade your copy of Shorebird.'; diff --git a/packages/shorebird_cli/lib/src/flutter_validation_mixin.dart b/packages/shorebird_cli/lib/src/flutter_validation_mixin.dart index 36ae61df..c3535b3f 100644 --- a/packages/shorebird_cli/lib/src/flutter_validation_mixin.dart +++ b/packages/shorebird_cli/lib/src/flutter_validation_mixin.dart @@ -6,7 +6,7 @@ mixin ShorebirdValidationMixin on ShorebirdCommand { /// Runs [Validator.validate] on all [validators] and writes issues to stdout. Future logValidationIssues() async { final validationIssues = (await Future.wait( - validators.map((v) => v.validate()), + validators.map((v) => v.validate(process)), )) .flattened; if (validationIssues.isNotEmpty) { diff --git a/packages/shorebird_cli/lib/src/shorebird_build_mixin.dart b/packages/shorebird_cli/lib/src/shorebird_build_mixin.dart index 5f2ea16b..14ff7047 100644 --- a/packages/shorebird_cli/lib/src/shorebird_build_mixin.dart +++ b/packages/shorebird_cli/lib/src/shorebird_build_mixin.dart @@ -1,38 +1,71 @@ import 'dart:io'; +import 'package:collection/collection.dart'; import 'package:mason_logger/mason_logger.dart'; import 'package:shorebird_cli/src/command.dart'; enum Arch { arm64, arm32, - x86, + x86_64, } class ArchMetadata { - const ArchMetadata({required this.path, required this.arch}); + const ArchMetadata({ + required this.path, + required this.arch, + required this.enginePath, + }); final String path; final String arch; + final String enginePath; } mixin ShorebirdBuildMixin on ShorebirdCommand { - // TODO(felangel): extend to other platforms. - static const architectures = { + // This exists only so tests can get the full list. + static const allAndroidArchitectures = { Arch.arm64: ArchMetadata( path: 'arm64-v8a', arch: 'aarch64', + enginePath: 'android_release_arm64', ), Arch.arm32: ArchMetadata( path: 'armeabi-v7a', arch: 'arm', + enginePath: 'android_release', ), - Arch.x86: ArchMetadata( + Arch.x86_64: ArchMetadata( path: 'x86_64', arch: 'x86_64', + enginePath: 'android_release_x64', ), }; + // TODO(felangel): extend to other platforms. + Map get architectures { + // Flutter has a whole bunch of logic to parse the --local-engine flag. + // We probably need similar. + // It's a bit odd to grab off the shorebird process, but it's the easiest + // way to have a single source of truth for the engine config for now. + if (engineConfig.localEngine != null) { + final localEngineOutName = engineConfig.localEngine; + final metaDataEntry = allAndroidArchitectures.entries.firstWhereOrNull( + (entry) => localEngineOutName == entry.value.enginePath, + ); + if (metaDataEntry == null) { + throw Exception( + 'Unknown local engine architecture for ' + '--local-engine=$localEngineOutName\n' + 'Known values: ' + '${allAndroidArchitectures.values.map((e) => e.enginePath)}', + ); + } + return {metaDataEntry.key: metaDataEntry.value}; + } + return allAndroidArchitectures; + } + Future buildAppBundle() async { const executable = 'flutter'; final arguments = [ @@ -42,7 +75,7 @@ mixin ShorebirdBuildMixin on ShorebirdCommand { ...results.rest, ]; - final result = await runProcess( + final result = await process.run( executable, arguments, runInShell: true, @@ -67,7 +100,7 @@ mixin ShorebirdBuildMixin on ShorebirdCommand { ...results.rest, ]; - final result = await runProcess( + final result = await process.run( executable, arguments, runInShell: true, diff --git a/packages/shorebird_cli/lib/src/shorebird_process.dart b/packages/shorebird_cli/lib/src/shorebird_process.dart index e231e09c..4aa9b95f 100644 --- a/packages/shorebird_cli/lib/src/shorebird_process.dart +++ b/packages/shorebird_cli/lib/src/shorebird_process.dart @@ -3,30 +3,34 @@ import 'dart:io'; import 'package:meta/meta.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, -}); +class EngineConfig { + const EngineConfig({ + required this.localEngineSrcPath, + required this.localEngine, + }); -typedef StartProcess = Future Function( - String executable, - List arguments, { - bool runInShell, - Map? environment, - bool useVendedFlutter, -}); + const EngineConfig.empty() + : localEngineSrcPath = null, + localEngine = null; + + final String? localEngineSrcPath; + final String? localEngine; +} /// A wrapper around [Process] that replaces executables to Shorebird-vended /// versions. -abstract class ShorebirdProcess { - @visibleForTesting - static ProcessWrapper processWrapper = ProcessWrapper(); +// This may need a better name, since it returns "Process" it's more a +// "ProcessFactory" than a "Process". +class ShorebirdProcess { + ShorebirdProcess({ + required this.engineConfig, + ProcessWrapper? processWrapper, // For mocking ShorebirdProcess. + }) : processWrapper = processWrapper ?? ProcessWrapper(); - static Future run( + final ProcessWrapper processWrapper; + final EngineConfig engineConfig; + + Future run( String executable, List arguments, { bool runInShell = false, @@ -44,16 +48,16 @@ abstract class ShorebirdProcess { return processWrapper.run( useVendedFlutter ? _resolveExecutable(executable) : executable, - arguments, + useVendedFlutter ? _resolveArguments(executable, arguments) : arguments, runInShell: runInShell, workingDirectory: workingDirectory, environment: resolvedEnvironment, ); } - static Future start( + Future start( String executable, - List argument, { + List arguments, { Map? environment, bool runInShell = false, bool useVendedFlutter = true, @@ -68,13 +72,13 @@ abstract class ShorebirdProcess { return processWrapper.start( useVendedFlutter ? _resolveExecutable(executable) : executable, - argument, + useVendedFlutter ? _resolveArguments(executable, arguments) : arguments, runInShell: runInShell, environment: resolvedEnvironment, ); } - static String _resolveExecutable(String executable) { + String _resolveExecutable(String executable) { if (executable == 'flutter') { return ShorebirdEnvironment.flutterBinaryFile.path; } @@ -82,7 +86,21 @@ abstract class ShorebirdProcess { return executable; } - static Map _environmentOverrides({ + List _resolveArguments( + String executable, + List arguments, + ) { + if (executable == 'flutter' && engineConfig.localEngine != null) { + return [ + '--local-engine-src-path=${engineConfig.localEngineSrcPath}', + '--local-engine=${engineConfig.localEngine}', + ...arguments + ]; + } + return arguments; + } + + Map _environmentOverrides({ required String executable, }) { if (executable == 'flutter') { diff --git a/packages/shorebird_cli/lib/src/shorebird_version_mixin.dart b/packages/shorebird_cli/lib/src/shorebird_version_mixin.dart index 786952d3..6a92117c 100644 --- a/packages/shorebird_cli/lib/src/shorebird_version_mixin.dart +++ b/packages/shorebird_cli/lib/src/shorebird_version_mixin.dart @@ -23,7 +23,7 @@ mixin ShorebirdVersionMixin on ShorebirdCommand { /// Exits if HEAD isn't pointing to a branch, or there is no upstream. Future fetchLatestGitHash({required String workingDirectory}) async { // Fetch upstream branch's commits and tags - await runProcess( + await process.run( 'git', ['fetch', '--tags'], workingDirectory: workingDirectory, @@ -47,7 +47,7 @@ mixin ShorebirdVersionMixin on ShorebirdCommand { String? workingDirectory, }) async { // Get the commit revision of HEAD - final result = await runProcess( + final result = await process.run( 'git', ['rev-parse', '--verify', revision], workingDirectory: workingDirectory, @@ -72,7 +72,7 @@ mixin ShorebirdVersionMixin on ShorebirdCommand { required String newRevision, required String workingDirectory, }) async { - final result = await runProcess( + final result = await process.run( 'git', ['reset', '--hard', newRevision], workingDirectory: workingDirectory, diff --git a/packages/shorebird_cli/lib/src/validators/android_internet_permission_validator.dart b/packages/shorebird_cli/lib/src/validators/android_internet_permission_validator.dart index b71ad764..87edf760 100644 --- a/packages/shorebird_cli/lib/src/validators/android_internet_permission_validator.dart +++ b/packages/shorebird_cli/lib/src/validators/android_internet_permission_validator.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:path/path.dart' as p; +import 'package:shorebird_cli/src/shorebird_process.dart'; import 'package:shorebird_cli/src/validators/validators.dart'; import 'package:xml/xml.dart'; @@ -16,7 +17,7 @@ class AndroidInternetPermissionValidator extends Validator { // coverage:ignore-end @override - Future> validate() async { + Future> validate(ShorebirdProcess process) async { const manifestFileName = 'AndroidManifest.xml'; final androidSrcDir = Directory( p.join( diff --git a/packages/shorebird_cli/lib/src/validators/shorebird_flutter_validator.dart b/packages/shorebird_cli/lib/src/validators/shorebird_flutter_validator.dart index ba8743c6..bdabc21d 100644 --- a/packages/shorebird_cli/lib/src/validators/shorebird_flutter_validator.dart +++ b/packages/shorebird_cli/lib/src/validators/shorebird_flutter_validator.dart @@ -10,9 +10,8 @@ class FlutterValidationException implements Exception { } class ShorebirdFlutterValidator extends Validator { - ShorebirdFlutterValidator({required this.runProcess}); + ShorebirdFlutterValidator(); - final RunProcess runProcess; final _flutterVersionRegex = RegExp(r'Flutter (\d+.\d+.\d+)'); // coverage:ignore-start @@ -21,7 +20,7 @@ class ShorebirdFlutterValidator extends Validator { // coverage:ignore-end @override - Future> validate() async { + Future> validate(ShorebirdProcess process) async { final issues = []; if (!ShorebirdEnvironment.flutterDirectory.existsSync()) { @@ -35,7 +34,7 @@ class ShorebirdFlutterValidator extends Validator { ); } - if (!await _flutterDirectoryIsClean()) { + if (!await _flutterDirectoryIsClean(process)) { issues.add( ValidationIssue( severity: ValidationIssueSeverity.warning, @@ -45,7 +44,7 @@ class ShorebirdFlutterValidator extends Validator { ); } - if (!await _flutterDirectoryTracksCorrectRevision()) { + if (!await _flutterDirectoryTracksCorrectRevision(process)) { final message = '''${ShorebirdEnvironment.flutterDirectory} is not on the correct revision'''; issues.add( @@ -56,8 +55,8 @@ class ShorebirdFlutterValidator extends Validator { ); } - final shorebirdFlutterVersion = await _shorebirdFlutterVersion(); - final pathFlutterVersion = await _pathFlutterVersion(); + final shorebirdFlutterVersion = await _shorebirdFlutterVersion(process); + final pathFlutterVersion = await _pathFlutterVersion(process); if (shorebirdFlutterVersion != pathFlutterVersion) { final message = ''' @@ -90,8 +89,8 @@ This can cause unexpected behavior if you are switching between the tools and th return issues; } - Future _flutterDirectoryIsClean() async { - final result = await runProcess( + Future _flutterDirectoryIsClean(ShorebirdProcess process) async { + final result = await process.run( 'git', ['status'], workingDirectory: ShorebirdEnvironment.flutterDirectory.path, @@ -101,8 +100,10 @@ This can cause unexpected behavior if you are switching between the tools and th .contains('nothing to commit, working tree clean'); } - Future _flutterDirectoryTracksCorrectRevision() async { - final result = await runProcess( + Future _flutterDirectoryTracksCorrectRevision( + ShorebirdProcess process, + ) async { + final result = await process.run( 'git', ['rev-parse', 'HEAD'], workingDirectory: ShorebirdEnvironment.flutterDirectory.path, @@ -112,18 +113,23 @@ This can cause unexpected behavior if you are switching between the tools and th .contains(ShorebirdEnvironment.flutterRevision); } - Future _shorebirdFlutterVersion() => _getFlutterVersion( + Future _shorebirdFlutterVersion(ShorebirdProcess process) => + _getFlutterVersion( + process: process, checkPathFlutter: false, ); - Future _pathFlutterVersion() => _getFlutterVersion( + Future _pathFlutterVersion(ShorebirdProcess process) => + _getFlutterVersion( + process: process, checkPathFlutter: true, ); Future _getFlutterVersion({ + required ShorebirdProcess process, required bool checkPathFlutter, }) async { - final result = await runProcess( + final result = await process.run( 'flutter', ['--version'], useVendedFlutter: !checkPathFlutter, diff --git a/packages/shorebird_cli/lib/src/validators/shorebird_version_validator.dart b/packages/shorebird_cli/lib/src/validators/shorebird_version_validator.dart index dbea103d..ee00a39d 100644 --- a/packages/shorebird_cli/lib/src/validators/shorebird_version_validator.dart +++ b/packages/shorebird_cli/lib/src/validators/shorebird_version_validator.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:path/path.dart' as p; +import 'package:shorebird_cli/src/shorebird_process.dart'; import 'package:shorebird_cli/src/validators/validators.dart'; /// Verifies that the currently installed version of Shorebird is the latest. @@ -16,7 +17,7 @@ class ShorebirdVersionValidator extends Validator { // coverage:ignore-end @override - Future> validate() async { + Future> validate(ShorebirdProcess process) async { final workingDirectory = p.dirname(Platform.script.toFilePath()); final bool isShorebirdUpToDate; diff --git a/packages/shorebird_cli/lib/src/validators/validators.dart b/packages/shorebird_cli/lib/src/validators/validators.dart index 4bd153f9..84feb902 100644 --- a/packages/shorebird_cli/lib/src/validators/validators.dart +++ b/packages/shorebird_cli/lib/src/validators/validators.dart @@ -1,5 +1,6 @@ import 'package:mason_logger/mason_logger.dart'; import 'package:meta/meta.dart'; +import 'package:shorebird_cli/src/shorebird_process.dart'; export 'android_internet_permission_validator.dart'; export 'shorebird_flutter_validator.dart'; @@ -74,5 +75,6 @@ abstract class Validator { /// Checks for [ValidationIssue]s. /// /// Returns an empty list if no issues are found. - Future> validate(); + /// Not all validators use [process]. + Future> validate(ShorebirdProcess process); } diff --git a/packages/shorebird_cli/test/src/commands/build/build_apk_command_test.dart b/packages/shorebird_cli/test/src/commands/build/build_apk_command_test.dart index 54da7752..d9116c1d 100644 --- a/packages/shorebird_cli/test/src/commands/build/build_apk_command_test.dart +++ b/packages/shorebird_cli/test/src/commands/build/build_apk_command_test.dart @@ -6,6 +6,7 @@ import 'package:mason_logger/mason_logger.dart'; import 'package:mocktail/mocktail.dart'; import 'package:shorebird_cli/src/auth/auth.dart'; import 'package:shorebird_cli/src/commands/build/build.dart'; +import 'package:shorebird_cli/src/shorebird_process.dart'; import 'package:shorebird_cli/src/validators/validators.dart'; import 'package:test/test.dart'; @@ -24,6 +25,8 @@ class _MockProcessResult extends Mock implements ProcessResult {} class _MockShorebirdFlutterValidator extends Mock implements ShorebirdFlutterValidator {} +class _MockShorebirdProcess extends Mock implements ShorebirdProcess {} + void main() { group('build apk', () { late ArgResults argResults; @@ -33,43 +36,39 @@ void main() { late ProcessResult processResult; late BuildApkCommand command; late ShorebirdFlutterValidator flutterValidator; - - String? processExecutable; - List? processArguments; + late ShorebirdProcess shorebirdProcess; setUp(() { argResults = _MockArgResults(); httpClient = _MockHttpClient(); auth = _MockAuth(); logger = _MockLogger(); + shorebirdProcess = _MockShorebirdProcess(); processResult = _MockProcessResult(); flutterValidator = _MockShorebirdFlutterValidator(); - processExecutable = null; - processArguments = null; command = BuildApkCommand( auth: auth, logger: logger, - runProcess: ( - executable, - arguments, { - bool runInShell = false, - Map? environment, - String? workingDirectory, - bool useVendedFlutter = true, - }) async { - processExecutable = executable; - processArguments = arguments; - return processResult; - }, validators: [flutterValidator], - )..testArgResults = argResults; + ) + ..testArgResults = argResults + ..testProcess = shorebirdProcess + ..testEngineConfig = const EngineConfig.empty(); + registerFallbackValue(shorebirdProcess); + when( + () => shorebirdProcess.run( + any(), + any(), + runInShell: any(named: 'runInShell'), + ), + ).thenAnswer((_) async => processResult); when(() => argResults.rest).thenReturn([]); when(() => auth.isAuthenticated).thenReturn(true); when(() => auth.client).thenReturn(httpClient); when(() => logger.progress(any())).thenReturn(_MockProgress()); when(() => logger.info(any())).thenReturn(null); - when(() => flutterValidator.validate()).thenAnswer((_) async => []); + when(() => flutterValidator.validate(any())).thenAnswer((_) async => []); }); test('has correct description', () { @@ -99,8 +98,13 @@ void main() { ); expect(result, equals(ExitCode.software.code)); - expect(processExecutable, equals('flutter')); - expect(processArguments, equals(['build', 'apk', '--release'])); + verify( + () => shorebirdProcess.run( + 'flutter', + ['build', 'apk', '--release'], + runInShell: any(named: 'runInShell'), + ), + ).called(1); }); test('exits with code 0 when building apk succeeds', () async { @@ -112,12 +116,17 @@ void main() { ); expect(result, equals(ExitCode.success.code)); - expect(processExecutable, equals('flutter')); - expect(processArguments, equals(['build', 'apk', '--release'])); + verify( + () => shorebirdProcess.run( + 'flutter', + ['build', 'apk', '--release'], + runInShell: any(named: 'runInShell'), + ), + ).called(1); }); test('prints flutter validation warnings', () async { - when(() => flutterValidator.validate()).thenAnswer( + when(() => flutterValidator.validate(any())).thenAnswer( (_) async => [ const ValidationIssue( severity: ValidationIssueSeverity.warning, diff --git a/packages/shorebird_cli/test/src/commands/build/build_app_bundle_command_test.dart b/packages/shorebird_cli/test/src/commands/build/build_app_bundle_command_test.dart index 0fb529fd..a4f713f3 100644 --- a/packages/shorebird_cli/test/src/commands/build/build_app_bundle_command_test.dart +++ b/packages/shorebird_cli/test/src/commands/build/build_app_bundle_command_test.dart @@ -6,6 +6,7 @@ import 'package:mason_logger/mason_logger.dart'; import 'package:mocktail/mocktail.dart'; import 'package:shorebird_cli/src/auth/auth.dart'; import 'package:shorebird_cli/src/commands/build/build.dart'; +import 'package:shorebird_cli/src/shorebird_process.dart'; import 'package:shorebird_cli/src/validators/validators.dart'; import 'package:test/test.dart'; @@ -24,6 +25,8 @@ class _MockProcessResult extends Mock implements ProcessResult {} class _MockShorebirdFlutterValidator extends Mock implements ShorebirdFlutterValidator {} +class _MockShorebirdProcess extends Mock implements ShorebirdProcess {} + void main() { group('build appbundle', () { late ArgResults argResults; @@ -33,9 +36,7 @@ void main() { late ProcessResult processResult; late BuildAppBundleCommand command; late ShorebirdFlutterValidator flutterValidator; - - String? processExecutable; - List? processArguments; + late ShorebirdProcess shorebirdProcess; setUp(() { argResults = _MockArgResults(); @@ -44,32 +45,31 @@ void main() { logger = _MockLogger(); processResult = _MockProcessResult(); flutterValidator = _MockShorebirdFlutterValidator(); - processExecutable = null; - processArguments = null; + shorebirdProcess = _MockShorebirdProcess(); command = BuildAppBundleCommand( auth: auth, logger: logger, - runProcess: ( - executable, - arguments, { - bool runInShell = false, - Map? environment, - String? workingDirectory, - bool useVendedFlutter = true, - }) async { - processExecutable = executable; - processArguments = arguments; - return processResult; - }, validators: [flutterValidator], - )..testArgResults = argResults; + ) + ..testArgResults = argResults + ..testProcess = shorebirdProcess + ..testEngineConfig = const EngineConfig.empty(); + registerFallbackValue(shorebirdProcess); + + when( + () => shorebirdProcess.run( + any(), + any(), + runInShell: any(named: 'runInShell'), + ), + ).thenAnswer((_) async => processResult); when(() => argResults.rest).thenReturn([]); when(() => auth.isAuthenticated).thenReturn(true); when(() => auth.client).thenReturn(httpClient); when(() => logger.progress(any())).thenReturn(_MockProgress()); when(() => logger.info(any())).thenReturn(null); - when(() => flutterValidator.validate()).thenAnswer((_) async => []); + when(() => flutterValidator.validate(any())).thenAnswer((_) async => []); }); test('has correct description', () { @@ -99,8 +99,13 @@ void main() { ); expect(result, equals(ExitCode.software.code)); - expect(processExecutable, equals('flutter')); - expect(processArguments, equals(['build', 'appbundle', '--release'])); + verify( + () => shorebirdProcess.run( + 'flutter', + ['build', 'appbundle', '--release'], + runInShell: any(named: 'runInShell'), + ), + ).called(1); }); test('exits with code 0 when building appbundle succeeds', () async { @@ -112,12 +117,34 @@ void main() { ); expect(result, equals(ExitCode.success.code)); - expect(processExecutable, equals('flutter')); - expect(processArguments, equals(['build', 'appbundle', '--release'])); + verify( + () => shorebirdProcess.run( + 'flutter', + ['build', 'appbundle', '--release'], + runInShell: any(named: 'runInShell'), + ), + ).called(1); + }); + + test('local-engine and architectures', () async { + expect(command.architectures.length, greaterThan(1)); + + command.testEngineConfig = const EngineConfig( + localEngine: 'android_release_arm64', + localEngineSrcPath: 'path/to/engine/src', + ); + expect(command.architectures.length, equals(1)); + + // We only support a few release configs for now. + command.testEngineConfig = const EngineConfig( + localEngine: 'android_debug_unopt', + localEngineSrcPath: 'path/to/engine/src', + ); + expect(() => command.architectures, throwsException); }); test('prints flutter validation warnings', () async { - when(() => flutterValidator.validate()).thenAnswer( + when(() => flutterValidator.validate(any())).thenAnswer( (_) async => [ const ValidationIssue( severity: ValidationIssueSeverity.warning, 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 f30fe419..a4e8f4a1 100644 --- a/packages/shorebird_cli/test/src/commands/doctor_command_test.dart +++ b/packages/shorebird_cli/test/src/commands/doctor_command_test.dart @@ -2,6 +2,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/shorebird_environment.dart'; +import 'package:shorebird_cli/src/shorebird_process.dart'; import 'package:shorebird_cli/src/validators/validators.dart'; import 'package:test/test.dart'; @@ -18,6 +19,8 @@ class _MockLogger extends Mock implements Logger {} class _MockProgress extends Mock implements Progress {} +class _MockShorebirdProcess extends Mock implements ShorebirdProcess {} + void main() { group('doctor', () { late Logger logger; @@ -26,6 +29,7 @@ void main() { late AndroidInternetPermissionValidator androidInternetPermissionValidator; late ShorebirdVersionValidator shorebirdVersionValidator; late ShorebirdFlutterValidator shorebirdFlutterValidator; + late ShorebirdProcess shorebirdProcess; setUp(() { logger = _MockLogger(); @@ -40,26 +44,28 @@ void main() { _MockAndroidInternetPermissionValidator(); shorebirdVersionValidator = _MockShorebirdVersionValidator(); shorebirdFlutterValidator = _MockShorebirdFlutterValidator(); + shorebirdProcess = _MockShorebirdProcess(); + registerFallbackValue(shorebirdProcess); when(() => androidInternetPermissionValidator.id) .thenReturn('$AndroidInternetPermissionValidator'); when(() => androidInternetPermissionValidator.description) .thenReturn('Android'); - when(() => androidInternetPermissionValidator.validate()) + when(() => androidInternetPermissionValidator.validate(any())) .thenAnswer((_) async => []); when(() => shorebirdVersionValidator.id) .thenReturn('$ShorebirdVersionValidator'); when(() => shorebirdVersionValidator.description) .thenReturn('Shorebird Version'); - when(() => shorebirdVersionValidator.validate()) + when(() => shorebirdVersionValidator.validate(any())) .thenAnswer((_) async => []); when(() => shorebirdFlutterValidator.id) .thenReturn('$ShorebirdFlutterValidator'); when(() => shorebirdFlutterValidator.description) .thenReturn('Shorebird Flutter'); - when(() => shorebirdFlutterValidator.validate()) + when(() => shorebirdFlutterValidator.validate(any())) .thenAnswer((_) async => []); command = DoctorCommand( @@ -69,13 +75,15 @@ void main() { shorebirdVersionValidator, shorebirdFlutterValidator, ], - ); + ) + ..testProcess = shorebirdProcess + ..testEngineConfig = const EngineConfig.empty(); }); test('prints "no issues" when everything is OK', () async { await command.run(); for (final validator in command.validators) { - verify(validator.validate).called(1); + verify(() => validator.validate(shorebirdProcess)).called(1); } verify( () => logger.info(any(that: contains('No issues detected'))), @@ -84,7 +92,7 @@ void main() { test('prints messages when warnings or errors found', () async { when( - () => androidInternetPermissionValidator.validate(), + () => androidInternetPermissionValidator.validate(any()), ).thenAnswer( (_) async => [ const ValidationIssue( @@ -101,7 +109,7 @@ void main() { await command.run(); for (final validator in command.validators) { - verify(validator.validate).called(1); + verify(() => validator.validate(any())).called(1); } verify( 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 f39a7ba5..7e9e901d 100644 --- a/packages/shorebird_cli/test/src/commands/patch_command_test.dart +++ b/packages/shorebird_cli/test/src/commands/patch_command_test.dart @@ -9,6 +9,7 @@ import 'package:shorebird_cli/src/auth/auth.dart'; import 'package:shorebird_cli/src/cache.dart' show Cache; import 'package:shorebird_cli/src/commands/patch_command.dart'; import 'package:shorebird_cli/src/shorebird_build_mixin.dart'; +import 'package:shorebird_cli/src/shorebird_process.dart'; import 'package:shorebird_cli/src/validators/validators.dart'; import 'package:shorebird_code_push_client/shorebird_code_push_client.dart'; import 'package:test/test.dart'; @@ -34,6 +35,8 @@ class _MockCodePushClient extends Mock implements CodePushClient {} class _MockShorebirdFlutterValidator extends Mock implements ShorebirdFlutterValidator {} +class _MockShorebirdProcess extends Mock implements ShorebirdProcess {} + void main() { group('patch', () { const appId = 'test-app-id'; @@ -91,6 +94,7 @@ flutter: late PatchCommand command; late Uri? capturedHostedUri; late ShorebirdFlutterValidator flutterValidator; + late ShorebirdProcess shorebirdProcess; Directory setUpTempDir() { final tempDir = Directory.systemTemp.createTempSync(); @@ -104,7 +108,8 @@ flutter: } void setUpTempArtifacts(Directory dir) { - for (final archMetadata in ShorebirdBuildMixin.architectures.values) { + for (final archMetadata + in ShorebirdBuildMixin.allAndroidArchitectures.values) { final artifactPath = p.join( dir.path, 'build', @@ -136,6 +141,7 @@ flutter: codePushClient = _MockCodePushClient(); flutterValidator = _MockShorebirdFlutterValidator(); cache = _MockCache(); + shorebirdProcess = _MockShorebirdProcess(); command = PatchCommand( auth: auth, buildCodePushClient: ({ @@ -146,23 +152,30 @@ flutter: return codePushClient; }, cache: cache, - runProcess: ( - executable, - arguments, { - bool runInShell = false, - Map? environment, - String? workingDirectory, - bool useVendedFlutter = true, - }) async { - if (executable == 'flutter') return flutterBuildProcessResult; - if (executable.endsWith('patch')) return patchProcessResult; - return _MockProcessResult(); - }, logger: logger, httpClient: httpClient, validators: [flutterValidator], - )..testArgResults = argResults; + ) + ..testArgResults = argResults + ..testProcess = shorebirdProcess + ..testEngineConfig = const EngineConfig.empty(); + registerFallbackValue(shorebirdProcess); + + when( + () => shorebirdProcess.run( + 'flutter', + any(), + runInShell: any(named: 'runInShell'), + ), + ).thenAnswer((_) async => flutterBuildProcessResult); + when( + () => shorebirdProcess.run( + any(that: endsWith('patch')), + any(), + runInShell: any(named: 'runInShell'), + ), + ).thenAnswer((_) async => patchProcessResult); when(() => argResults.rest).thenReturn([]); when(() => argResults['arch']).thenReturn(arch); when(() => argResults['platform']).thenReturn(platform); @@ -223,7 +236,7 @@ flutter: channelId: any(named: 'channelId'), ), ).thenAnswer((_) async {}); - when(() => flutterValidator.validate()).thenAnswer((_) async => []); + when(() => flutterValidator.validate(any())).thenAnswer((_) async => []); when(() => cache.updateAll()).thenAnswer((_) async => {}); when( () => cache.getArtifactDirectory(any()), @@ -580,7 +593,7 @@ base_url: $baseUrl''', test('prints flutter validation warnings', () async { final tempDir = setUpTempDir(); setUpTempArtifacts(tempDir); - when(() => flutterValidator.validate()).thenAnswer( + when(() => flutterValidator.validate(any())).thenAnswer( (_) async => [ const ValidationIssue( severity: ValidationIssueSeverity.warning, 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 833b0352..28391425 100644 --- a/packages/shorebird_cli/test/src/commands/release_command_test.dart +++ b/packages/shorebird_cli/test/src/commands/release_command_test.dart @@ -8,6 +8,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/shorebird_build_mixin.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'; import 'package:test/test.dart'; @@ -29,6 +30,8 @@ class _MockCodePushClient extends Mock implements CodePushClient {} class _MockShorebirdFlutterValidator extends Mock implements ShorebirdFlutterValidator {} +class _MockShorebirdProcess extends Mock implements ShorebirdProcess {} + void main() { group('release', () { const appId = 'test-app-id'; @@ -73,6 +76,7 @@ flutter: late ReleaseCommand command; late Uri? capturedHostedUri; late ShorebirdFlutterValidator flutterValidator; + late ShorebirdProcess shorebirdProcess; Directory setUpTempDir() { final tempDir = Directory.systemTemp.createTempSync(); @@ -86,7 +90,8 @@ flutter: } void setUpTempArtifacts(Directory dir) { - for (final archMetadata in ShorebirdBuildMixin.architectures.values) { + for (final archMetadata + in ShorebirdBuildMixin.allAndroidArchitectures.values) { final artifactPath = p.join( dir.path, 'build', @@ -112,6 +117,7 @@ flutter: processResult = _MockProcessResult(); codePushClient = _MockCodePushClient(); flutterValidator = _MockShorebirdFlutterValidator(); + shorebirdProcess = _MockShorebirdProcess(); command = ReleaseCommand( auth: auth, buildCodePushClient: ({ @@ -121,20 +127,22 @@ flutter: capturedHostedUri = hostedUri; return codePushClient; }, - runProcess: ( - executable, - arguments, { - bool runInShell = false, - Map? environment, - String? workingDirectory, - bool useVendedFlutter = true, - }) async { - return processResult; - }, logger: logger, validators: [flutterValidator], - )..testArgResults = argResults; + ) + ..testArgResults = argResults + ..testProcess = shorebirdProcess + ..testEngineConfig = const EngineConfig.empty(); + registerFallbackValue(shorebirdProcess); + + when( + () => shorebirdProcess.run( + any(), + any(), + runInShell: any(named: 'runInShell'), + ), + ).thenAnswer((_) async => processResult); when(() => argResults.rest).thenReturn([]); when(() => argResults['arch']).thenReturn(arch); when(() => argResults['platform']).thenReturn(platform); @@ -167,7 +175,7 @@ flutter: hash: any(named: 'hash'), ), ).thenAnswer((_) async => releaseArtifact); - when(() => flutterValidator.validate()).thenAnswer((_) async => []); + when(() => flutterValidator.validate(any())).thenAnswer((_) async => []); }); test('throws config error when shorebird is not initialized', () async { @@ -331,7 +339,7 @@ Did you forget to run "shorebird init"?''', }); test('prints flutter validation warnings', () async { - when(() => flutterValidator.validate()).thenAnswer( + when(() => flutterValidator.validate(any())).thenAnswer( (_) async => [ const ValidationIssue( severity: ValidationIssueSeverity.warning, 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 d8af4d68..71a838b5 100644 --- a/packages/shorebird_cli/test/src/commands/run_command_test.dart +++ b/packages/shorebird_cli/test/src/commands/run_command_test.dart @@ -8,6 +8,7 @@ import 'package:mason_logger/mason_logger.dart'; import 'package:mocktail/mocktail.dart'; import 'package:shorebird_cli/src/auth/auth.dart'; import 'package:shorebird_cli/src/commands/run_command.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'; import 'package:test/test.dart'; @@ -32,6 +33,8 @@ class _MockAndroidInternetPermissionValidator extends Mock class _MockShorebirdFlutterValidator extends Mock implements ShorebirdFlutterValidator {} +class _MockShorebirdProcess extends Mock implements ShorebirdProcess {} + void main() { group('run', () { late ArgResults argResults; @@ -43,6 +46,7 @@ void main() { late RunCommand runCommand; late AndroidInternetPermissionValidator androidInternetPermissionValidator; late ShorebirdFlutterValidator flutterValidator; + late ShorebirdProcess shorebirdProcess; setUp(() { argResults = _MockArgResults(); @@ -50,6 +54,7 @@ void main() { auth = _MockAuth(); logger = _MockLogger(); process = _MockProcess(); + shorebirdProcess = _MockShorebirdProcess(); codePushClient = _MockCodePushClient(); androidInternetPermissionValidator = _MockAndroidInternetPermissionValidator(); @@ -63,23 +68,32 @@ void main() { }) { return codePushClient; }, - startProcess: (executable, arguments, {bool runInShell = false}) async { - return process; - }, validators: [ androidInternetPermissionValidator, flutterValidator, ], - )..testArgResults = argResults; + ) + ..testArgResults = argResults + ..testProcess = shorebirdProcess + ..testEngineConfig = const EngineConfig.empty(); + registerFallbackValue(shorebirdProcess); + + when( + () => shorebirdProcess.start( + any(), + any(), + runInShell: any(named: 'runInShell'), + ), + ).thenAnswer((_) async => process); when(() => argResults.rest).thenReturn([]); when(() => auth.isAuthenticated).thenReturn(true); when(() => auth.client).thenReturn(httpClient); when(() => logger.progress(any())).thenReturn(_MockProgress()); when( - () => androidInternetPermissionValidator.validate(), + () => androidInternetPermissionValidator.validate(any()), ).thenAnswer((_) async => []); - when(() => flutterValidator.validate()).thenAnswer((_) async => []); + when(() => flutterValidator.validate(any())).thenAnswer((_) async => []); }); test('exits with no user when not logged in', () async { @@ -145,7 +159,7 @@ void main() { }); test('prints validation warnings', () async { - when(() => flutterValidator.validate()).thenAnswer( + when(() => flutterValidator.validate(any())).thenAnswer( (_) async => [ const ValidationIssue( severity: ValidationIssueSeverity.warning, @@ -153,7 +167,7 @@ void main() { ), ], ); - when(() => androidInternetPermissionValidator.validate()).thenAnswer( + when(() => androidInternetPermissionValidator.validate(any())).thenAnswer( (_) async => [ const ValidationIssue( severity: ValidationIssueSeverity.error, 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 de1eff27..4320e25e 100644 --- a/packages/shorebird_cli/test/src/commands/upgrade_command_test.dart +++ b/packages/shorebird_cli/test/src/commands/upgrade_command_test.dart @@ -3,6 +3,7 @@ import 'dart:io'; 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/shorebird_process.dart'; import 'package:test/test.dart'; class _MockLogger extends Mock implements Logger {} @@ -11,6 +12,8 @@ class _MockProcessResult extends Mock implements ProcessResult {} class _MockProgress extends Mock implements Progress {} +class _MockShorebirdProcess extends Mock implements ShorebirdProcess {} + void main() { const currentShorebirdRevision = 'revision-1'; const newerShorebirdRevision = 'revision-2'; @@ -22,6 +25,7 @@ void main() { late ProcessResult fetchLatestVersionResult; late ProcessResult hardResetResult; late UpgradeCommand command; + late ShorebirdProcess shorebirdProcess; setUp(() { final progress = _MockProgress(); @@ -32,40 +36,41 @@ void main() { fetchTagsResult = _MockProcessResult(); fetchLatestVersionResult = _MockProcessResult(); hardResetResult = _MockProcessResult(); + shorebirdProcess = _MockShorebirdProcess(); command = UpgradeCommand( logger: logger, - runProcess: ( - executable, - arguments, { - bool runInShell = false, - Map? environment, - workingDirectory, - bool useVendedFlutter = true, - }) async { - if (executable == 'git') { - const revParseHead = ['rev-parse', '--verify', 'HEAD']; - if (arguments.every((arg) => revParseHead.contains(arg))) { - return fetchCurrentVersionResult; - } + ) + ..testProcess = shorebirdProcess + ..testEngineConfig = const EngineConfig.empty(); - const fetchTags = ['fetch', '--tags']; - if (arguments.every((arg) => fetchTags.contains(arg))) { - return fetchTagsResult; - } - - const revParseUpstream = ['rev-parse', '--verify', '@{upstream}']; - if (arguments.every((arg) => revParseUpstream.contains(arg))) { - return fetchLatestVersionResult; - } - - const resetHard = ['reset', '--hard', newerShorebirdRevision]; - if (arguments.every((arg) => resetHard.contains(arg))) { - return hardResetResult; - } - } - return _MockProcessResult(); - }, - ); + when( + () => shorebirdProcess.run( + 'git', + ['rev-parse', '--verify', 'HEAD'], + workingDirectory: any(named: 'workingDirectory'), + ), + ).thenAnswer((_) async => fetchCurrentVersionResult); + when( + () => shorebirdProcess.run( + 'git', + ['fetch', '--tags'], + workingDirectory: any(named: 'workingDirectory'), + ), + ).thenAnswer((_) async => fetchTagsResult); + when( + () => shorebirdProcess.run( + 'git', + ['rev-parse', '--verify', '@{upstream}'], + workingDirectory: any(named: 'workingDirectory'), + ), + ).thenAnswer((_) async => fetchLatestVersionResult); + when( + () => shorebirdProcess.run( + 'git', + ['reset', '--hard', newerShorebirdRevision], + workingDirectory: any(named: 'workingDirectory'), + ), + ).thenAnswer((_) async => hardResetResult); when( () => fetchCurrentVersionResult.exitCode, diff --git a/packages/shorebird_cli/test/src/shorebird_process_test.dart b/packages/shorebird_cli/test/src/shorebird_process_test.dart index fac92fde..48ba036a 100644 --- a/packages/shorebird_cli/test/src/shorebird_process_test.dart +++ b/packages/shorebird_cli/test/src/shorebird_process_test.dart @@ -15,13 +15,16 @@ void main() { late ProcessWrapper processWrapper; late Process startProcess; late ProcessResult runProcessResult; + late ShorebirdProcess shorebirdProcess; setUp(() { processWrapper = _MockProcessWrapper(); runProcessResult = _MockProcessResult(); startProcess = _MockProcess(); - - ShorebirdProcess.processWrapper = processWrapper; + shorebirdProcess = ShorebirdProcess( + processWrapper: processWrapper, + engineConfig: const EngineConfig.empty(), + ); when( () => processWrapper.run( @@ -45,7 +48,7 @@ void main() { group('run', () { test('forwards non-flutter executables to Process.run', () async { - await ShorebirdProcess.run( + await shorebirdProcess.run( 'git', ['pull'], runInShell: true, @@ -64,7 +67,7 @@ void main() { }); test('replaces "flutter" with our local flutter', () async { - await ShorebirdProcess.run( + await shorebirdProcess.run( 'flutter', ['--version'], runInShell: true, @@ -87,7 +90,7 @@ void main() { test( 'does not replace flutter with our local flutter if' ' useVendedFlutter is false', () async { - await ShorebirdProcess.run( + await shorebirdProcess.run( 'flutter', ['--version'], runInShell: true, @@ -107,7 +110,7 @@ void main() { }); test('Updates environment if useVendedFlutter is true', () async { - await ShorebirdProcess.run( + await shorebirdProcess.run( 'flutter', ['--version'], runInShell: true, @@ -130,7 +133,7 @@ void main() { test( 'Makes no changes to environment if useVendedFlutter is false', () async { - await ShorebirdProcess.run( + await shorebirdProcess.run( 'flutter', ['--version'], runInShell: true, @@ -152,9 +155,34 @@ void main() { ); }); + test('adds local-engine arguments if set', () async { + shorebirdProcess = ShorebirdProcess( + processWrapper: processWrapper, + engineConfig: const EngineConfig( + localEngineSrcPath: '/path/to/engine/src', + localEngine: 'android_release_arm64', + ), + ); + + await shorebirdProcess.run('flutter', []); + + verify( + () => processWrapper.run( + any(), + [ + '--local-engine-src-path=/path/to/engine/src', + '--local-engine=android_release_arm64', + ], + runInShell: any(named: 'runInShell'), + environment: any(named: 'environment'), + workingDirectory: any(named: 'workingDirectory'), + ), + ).called(1); + }); + group('start', () { test('forwards non-flutter executables to Process.run', () async { - await ShorebirdProcess.start('git', ['pull'], runInShell: true); + await shorebirdProcess.start('git', ['pull'], runInShell: true); verify( () => processWrapper.start( @@ -167,7 +195,7 @@ void main() { }); test('replaces "flutter" with our local flutter', () async { - await ShorebirdProcess.start('flutter', ['run'], runInShell: true); + await shorebirdProcess.start('flutter', ['run'], runInShell: true); verify( () => processWrapper.start( @@ -184,7 +212,7 @@ void main() { test( 'does not replace flutter with our local flutter if' ' useVendedFlutter is false', () async { - await ShorebirdProcess.start( + await shorebirdProcess.start( 'flutter', ['--version'], runInShell: true, @@ -203,7 +231,7 @@ void main() { }); test('Updates environment if useVendedFlutter is true', () async { - await ShorebirdProcess.start( + await shorebirdProcess.start( 'flutter', ['--version'], runInShell: true, @@ -226,7 +254,7 @@ void main() { test( 'Makes no changes to environment if useVendedFlutter is false', () async { - await ShorebirdProcess.start( + await shorebirdProcess.start( 'flutter', ['--version'], runInShell: true, diff --git a/packages/shorebird_cli/test/src/validators/android_internet_permission_validator_test.dart b/packages/shorebird_cli/test/src/validators/android_internet_permission_validator_test.dart index 25b87aa3..e404949d 100644 --- a/packages/shorebird_cli/test/src/validators/android_internet_permission_validator_test.dart +++ b/packages/shorebird_cli/test/src/validators/android_internet_permission_validator_test.dart @@ -1,10 +1,14 @@ import 'dart:io'; import 'package:collection/collection.dart'; +import 'package:mocktail/mocktail.dart'; import 'package:path/path.dart' as p; +import 'package:shorebird_cli/src/shorebird_process.dart'; import 'package:shorebird_cli/src/validators/validators.dart'; import 'package:test/test.dart'; +class _MockShorebirdProcess extends Mock implements ShorebirdProcess {} + void main() { const manifestWithInternetPermission = ''' Directory.systemTemp.createTempSync(); void writeManifestToPath(String manifestContents, String path) { @@ -57,7 +67,7 @@ void main() { ); final results = await IOOverrides.runZoned( - () => AndroidInternetPermissionValidator().validate(), + () => AndroidInternetPermissionValidator().validate(shorebirdProcess), getCurrentDirectory: () => tempDirectory, ); @@ -66,7 +76,8 @@ void main() { ); test('returns an error if no android project is found', () async { - final results = await AndroidInternetPermissionValidator().validate(); + final results = + await AndroidInternetPermissionValidator().validate(shorebirdProcess); expect(results, hasLength(1)); expect(results.first.severity, ValidationIssueSeverity.error); @@ -80,7 +91,7 @@ void main() { .createSync(recursive: true); final results = await IOOverrides.runZoned( - () => AndroidInternetPermissionValidator().validate(), + () => AndroidInternetPermissionValidator().validate(shorebirdProcess), getCurrentDirectory: () => tempDirectory, ); @@ -133,7 +144,7 @@ void main() { ); final results = await IOOverrides.runZoned( - () => AndroidInternetPermissionValidator().validate(), + () => AndroidInternetPermissionValidator().validate(shorebirdProcess), getCurrentDirectory: () => tempDirectory, ); diff --git a/packages/shorebird_cli/test/src/validators/shorebird_flutter_validator_test.dart b/packages/shorebird_cli/test/src/validators/shorebird_flutter_validator_test.dart index 07023796..110dc231 100644 --- a/packages/shorebird_cli/test/src/validators/shorebird_flutter_validator_test.dart +++ b/packages/shorebird_cli/test/src/validators/shorebird_flutter_validator_test.dart @@ -1,10 +1,10 @@ import 'dart:io' hide Platform; -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/shorebird_environment.dart'; +import 'package:shorebird_cli/src/shorebird_process.dart'; import 'package:shorebird_cli/src/validators/validators.dart'; import 'package:test/test.dart'; @@ -12,6 +12,8 @@ class _MockProcessResult extends Mock implements ProcessResult {} class _MockPlatform extends Mock implements Platform {} +class _MockShorebirdProcess extends Mock implements ShorebirdProcess {} + void main() { group('ShorebirdFlutterValidator', () { const flutterRevision = '45fc514f1a9c347a3af76b02baf980a4d88b7879'; @@ -44,6 +46,7 @@ Tools • Dart 2.19.6 • DevTools 2.20.1 late ProcessResult shorebirdFlutterVersionProcessResult; late ProcessResult gitRevParseHeadProcessResult; late ProcessResult gitStatusProcessResult; + late ShorebirdProcess shorebirdProcess; Directory flutterDirectory(Directory root) => Directory(p.join(root.path, 'bin', 'cache', 'flutter')); @@ -71,34 +74,32 @@ Tools • Dart 2.19.6 • DevTools 2.20.1 shorebirdFlutterVersionProcessResult = _MockProcessResult(); gitRevParseHeadProcessResult = _MockProcessResult(); gitStatusProcessResult = _MockProcessResult(); + shorebirdProcess = _MockShorebirdProcess(); - validator = ShorebirdFlutterValidator( - runProcess: ( - executable, - arguments, { - bool runInShell = false, - Map? environment, - workingDirectory, - bool useVendedFlutter = true, - }) async { - if (executable == 'git') { - if (arguments.equals(['status'])) { - return gitStatusProcessResult; - } else if (arguments.equals(['rev-parse', 'HEAD'])) { - return gitRevParseHeadProcessResult; - } - } else if (executable == 'flutter') { - if (arguments.equals(['--version'])) { - if (useVendedFlutter) { - return shorebirdFlutterVersionProcessResult; - } else { - return pathFlutterVersionProcessResult; - } - } - } - return _MockProcessResult(); - }, - ); + validator = ShorebirdFlutterValidator(); + when( + () => shorebirdProcess.run( + 'git', + ['rev-parse', 'HEAD'], + workingDirectory: any(named: 'workingDirectory'), + ), + ).thenAnswer((_) async => gitRevParseHeadProcessResult); + when( + () => shorebirdProcess.run( + 'git', + ['status'], + workingDirectory: any(named: 'workingDirectory'), + ), + ).thenAnswer((_) async => gitStatusProcessResult); + when(() => shorebirdProcess.run('flutter', ['--version'])) + .thenAnswer((_) async => shorebirdFlutterVersionProcessResult); + when( + () => shorebirdProcess.run( + 'flutter', + ['--version'], + useVendedFlutter: false, + ), + ).thenAnswer((_) async => pathFlutterVersionProcessResult); when(() => pathFlutterVersionProcessResult.stdout) .thenReturn(pathFlutterVersionMessage); @@ -114,7 +115,7 @@ Tools • Dart 2.19.6 • DevTools 2.20.1 }); test('returns no issues when the Flutter install is good', () async { - final results = await validator.validate(); + final results = await validator.validate(shorebirdProcess); expect(results, isEmpty); }); @@ -122,7 +123,7 @@ Tools • Dart 2.19.6 • DevTools 2.20.1 test('errors when Flutter does not exist', () async { flutterDirectory(tempDir).deleteSync(); - final results = await validator.validate(); + final results = await validator.validate(shorebirdProcess); expect(results, hasLength(1)); expect(results.first.severity, ValidationIssueSeverity.error); @@ -133,7 +134,7 @@ Tools • Dart 2.19.6 • DevTools 2.20.1 when(() => gitStatusProcessResult.stdout) .thenReturn('Changes not staged for commit'); - final results = await validator.validate(); + final results = await validator.validate(shorebirdProcess); expect(results, hasLength(1)); expect(results.first.severity, ValidationIssueSeverity.warning); @@ -145,7 +146,7 @@ Tools • Dart 2.19.6 • DevTools 2.20.1 62bd79521d '''); - final results = await validator.validate(); + final results = await validator.validate(shorebirdProcess); expect(results, hasLength(1)); expect(results.first.severity, ValidationIssueSeverity.warning); @@ -160,14 +161,15 @@ Tools • Dart 2.19.6 • DevTools 2.20.1 pathFlutterVersionMessage.replaceAll('3.7.9', '3.7.10'), ); - final results = await validator.validate(); + final results = await validator.validate(shorebirdProcess); expect(results, hasLength(1)); expect(results.first.severity, ValidationIssueSeverity.warning); expect( results.first.message, contains( - 'The version of Flutter that Shorebird includes and the Flutter on your path are different', + 'The version of Flutter that Shorebird includes and the Flutter on ' + 'your path are different', ), ); }, @@ -180,7 +182,7 @@ Tools • Dart 2.19.6 • DevTools 2.20.1 {'FLUTTER_STORAGE_BASE_URL': 'https://storage.flutter-io.cn'}, ); - final results = await validator.validate(); + final results = await validator.validate(shorebirdProcess); expect(results, hasLength(1)); expect(results.first.severity, ValidationIssueSeverity.warning); @@ -198,7 +200,7 @@ Tools • Dart 2.19.6 • DevTools 2.20.1 when(() => pathFlutterVersionProcessResult.stdout) .thenReturn('OH NO THERE IS NO FLUTTER VERSION HERE'); - expect(() async => validator.validate(), throwsException); + expect(() async => validator.validate(shorebirdProcess), throwsException); }); test('prints stderr output and throws if version check fails', () async { @@ -207,7 +209,7 @@ Tools • Dart 2.19.6 • DevTools 2.20.1 .thenReturn('error getting Flutter version'); expect( - () async => validator.validate(), + () async => validator.validate(shorebirdProcess), throwsA( isA().having( (e) => e.message, diff --git a/packages/shorebird_cli/test/src/validators/shorebird_version_validator_test.dart b/packages/shorebird_cli/test/src/validators/shorebird_version_validator_test.dart index c3d77710..cf8df271 100644 --- a/packages/shorebird_cli/test/src/validators/shorebird_version_validator_test.dart +++ b/packages/shorebird_cli/test/src/validators/shorebird_version_validator_test.dart @@ -3,6 +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/shorebird_process.dart'; import 'package:shorebird_cli/src/validators/validators.dart'; import 'package:test/test.dart'; @@ -10,6 +11,8 @@ class _MockLogger extends Mock implements Logger {} class _MockProcessResult extends Mock implements ProcessResult {} +class _MockShorebirdProcess extends Mock implements ShorebirdProcess {} + void main() { const currentShorebirdRevision = 'revision-1'; const newerShorebirdRevision = 'revision-2'; @@ -20,41 +23,46 @@ void main() { late DoctorCommand command; late ProcessResult fetchCurrentVersionResult; late ProcessResult fetchLatestVersionResult; + late ShorebirdProcess shorebirdProcess; setUp(() { logger = _MockLogger(); fetchCurrentVersionResult = _MockProcessResult(); fetchLatestVersionResult = _MockProcessResult(); + shorebirdProcess = _MockShorebirdProcess(); command = DoctorCommand( logger: logger, - runProcess: ( - executable, - arguments, { - bool runInShell = false, - Map? environment, - workingDirectory, - bool useVendedFlutter = true, - }) async { - if (executable == 'git') { - const revParseHead = ['rev-parse', '--verify', 'HEAD']; - if (arguments.every((arg) => revParseHead.contains(arg))) { - return fetchCurrentVersionResult; - } - - const revParseUpstream = ['rev-parse', '--verify', '@{upstream}']; - if (arguments.every((arg) => revParseUpstream.contains(arg))) { - return fetchLatestVersionResult; - } - } - return _MockProcessResult(); - }, - ); + ) + ..testProcess = shorebirdProcess + ..testEngineConfig = const EngineConfig.empty(); validator = ShorebirdVersionValidator( isShorebirdVersionCurrent: command.isShorebirdVersionCurrent, ); + when( + () => shorebirdProcess.run( + 'git', + ['rev-parse', '--verify', 'HEAD'], + workingDirectory: any(named: 'workingDirectory'), + ), + ).thenAnswer((_) async => fetchCurrentVersionResult); + when( + () => shorebirdProcess.run( + 'git', + ['rev-parse', '--verify', '@{upstream}'], + workingDirectory: any(named: 'workingDirectory'), + ), + ).thenAnswer((_) async => fetchLatestVersionResult); + when( + () => shorebirdProcess.run( + 'git', + ['fetch', '--tags'], + workingDirectory: any(named: 'workingDirectory'), + ), + ).thenAnswer((_) async => _MockProcessResult()); + when( () => fetchCurrentVersionResult.exitCode, ).thenReturn(ExitCode.success.code); @@ -70,7 +78,7 @@ void main() { }); test('returns no issues when shorebird is up-to-date', () async { - final results = await validator.validate(); + final results = await validator.validate(shorebirdProcess); expect(results, isEmpty); }); @@ -79,7 +87,7 @@ void main() { () => fetchLatestVersionResult.stdout, ).thenReturn(newerShorebirdRevision); - final results = await validator.validate(); + final results = await validator.validate(shorebirdProcess); expect(results, hasLength(1)); expect(results.first.severity, ValidationIssueSeverity.warning); expect( @@ -101,7 +109,7 @@ void main() { ), ); - final results = await validator.validate(); + final results = await validator.validate(shorebirdProcess); expect(results, hasLength(1)); expect(results.first.severity, ValidationIssueSeverity.error);