feat: Add a workflow for local-engine dev (#309)

This commit is contained in:
Eric Seidel
2023-04-19 14:40:53 -04:00
committed by GitHub
parent 603c1113e9
commit f57449effc
30 changed files with 535 additions and 300 deletions
+16 -8
View File
@@ -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
+33 -16
View File
@@ -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<Process> Function(
bool runInShell,
});
List<Validator> _defaultValidators({required RunProcess runProcess}) {
return [
ShorebirdFlutterValidator(runProcess: runProcess),
AndroidInternetPermissionValidator(),
];
}
List<Validator> _defaultValidators() => [
ShorebirdFlutterValidator(),
AndroidInternetPermissionValidator(),
];
abstract class ShorebirdCommand extends Command<int> {
ShorebirdCommand({
@@ -38,24 +37,42 @@ abstract class ShorebirdCommand extends Command<int> {
Auth? auth,
Cache? cache,
CodePushClientBuilder? buildCodePushClient,
RunProcess? runProcess,
StartProcess? startProcess,
List<Validator>? validators,
List<Validator>? 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<Validator> validators;
@@ -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<int> {
..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<int> {
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<int> run(Iterable<String> args) async {
@@ -64,6 +80,15 @@ class ShorebirdCliCommandRunner extends CompletionCommandRunner<int> {
_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
@@ -17,7 +17,6 @@ class BuildApkCommand extends ShorebirdCommand
BuildApkCommand({
required super.logger,
super.auth,
super.runProcess,
super.validators,
});
@@ -17,7 +17,6 @@ class BuildAppBundleCommand extends ShorebirdCommand
BuildAppBundleCommand({
required super.logger,
super.auth,
super.runProcess,
super.validators,
});
@@ -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,
),
);
@@ -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();
@@ -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,
@@ -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',
@@ -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;
}
}
@@ -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.';
@@ -6,7 +6,7 @@ mixin ShorebirdValidationMixin on ShorebirdCommand {
/// Runs [Validator.validate] on all [validators] and writes issues to stdout.
Future<void> logValidationIssues() async {
final validationIssues = (await Future.wait(
validators.map((v) => v.validate()),
validators.map((v) => v.validate(process)),
))
.flattened;
if (validationIssues.isNotEmpty) {
@@ -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 = <Arch, ArchMetadata>{
// This exists only so tests can get the full list.
static const allAndroidArchitectures = <Arch, ArchMetadata>{
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<Arch, ArchMetadata> 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<void> 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,
@@ -3,30 +3,34 @@ import 'dart:io';
import 'package:meta/meta.dart';
import 'package:shorebird_cli/src/shorebird_environment.dart';
typedef RunProcess = Future<ProcessResult> Function(
String executable,
List<String> arguments, {
bool runInShell,
Map<String, String>? environment,
String? workingDirectory,
bool useVendedFlutter,
});
class EngineConfig {
const EngineConfig({
required this.localEngineSrcPath,
required this.localEngine,
});
typedef StartProcess = Future<Process> Function(
String executable,
List<String> arguments, {
bool runInShell,
Map<String, String>? 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<ProcessResult> run(
final ProcessWrapper processWrapper;
final EngineConfig engineConfig;
Future<ProcessResult> run(
String executable,
List<String> 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<Process> start(
Future<Process> start(
String executable,
List<String> argument, {
List<String> arguments, {
Map<String, String>? 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<String, String> _environmentOverrides({
List<String> _resolveArguments(
String executable,
List<String> arguments,
) {
if (executable == 'flutter' && engineConfig.localEngine != null) {
return [
'--local-engine-src-path=${engineConfig.localEngineSrcPath}',
'--local-engine=${engineConfig.localEngine}',
...arguments
];
}
return arguments;
}
Map<String, String> _environmentOverrides({
required String executable,
}) {
if (executable == 'flutter') {
@@ -23,7 +23,7 @@ mixin ShorebirdVersionMixin on ShorebirdCommand {
/// Exits if HEAD isn't pointing to a branch, or there is no upstream.
Future<String> 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,
@@ -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<List<ValidationIssue>> validate() async {
Future<List<ValidationIssue>> validate(ShorebirdProcess process) async {
const manifestFileName = 'AndroidManifest.xml';
final androidSrcDir = Directory(
p.join(
@@ -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<List<ValidationIssue>> validate() async {
Future<List<ValidationIssue>> validate(ShorebirdProcess process) async {
final issues = <ValidationIssue>[];
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<bool> _flutterDirectoryIsClean() async {
final result = await runProcess(
Future<bool> _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<bool> _flutterDirectoryTracksCorrectRevision() async {
final result = await runProcess(
Future<bool> _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<String> _shorebirdFlutterVersion() => _getFlutterVersion(
Future<String> _shorebirdFlutterVersion(ShorebirdProcess process) =>
_getFlutterVersion(
process: process,
checkPathFlutter: false,
);
Future<String> _pathFlutterVersion() => _getFlutterVersion(
Future<String> _pathFlutterVersion(ShorebirdProcess process) =>
_getFlutterVersion(
process: process,
checkPathFlutter: true,
);
Future<String> _getFlutterVersion({
required ShorebirdProcess process,
required bool checkPathFlutter,
}) async {
final result = await runProcess(
final result = await process.run(
'flutter',
['--version'],
useVendedFlutter: !checkPathFlutter,
@@ -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<List<ValidationIssue>> validate() async {
Future<List<ValidationIssue>> validate(ShorebirdProcess process) async {
final workingDirectory = p.dirname(Platform.script.toFilePath());
final bool isShorebirdUpToDate;
@@ -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<List<ValidationIssue>> validate();
/// Not all validators use [process].
Future<List<ValidationIssue>> validate(ShorebirdProcess process);
}
@@ -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<String>? 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<String, String>? 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,
@@ -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<String>? 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<String, String>? 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,
@@ -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(
@@ -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<String, String>? 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,
@@ -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<String, String>? 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,
@@ -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,
@@ -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<String, String>? 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,
@@ -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,
@@ -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 = '''
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
@@ -34,6 +38,12 @@ void main() {
''';
group('AndroidInternetPermissionValidator', () {
late ShorebirdProcess shorebirdProcess;
setUp(() {
shorebirdProcess = _MockShorebirdProcess();
});
Directory createTempDir() => 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,
);
@@ -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<String, String>? 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<FlutterValidationException>().having(
(e) => e.message,
@@ -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<String, String>? 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);