feat(shorebird_cli): add build command (#26)

This commit is contained in:
Felix Angelov
2023-03-06 16:41:23 -06:00
committed by GitHub
parent 2bf8850fd5
commit debc601066
6 changed files with 213 additions and 2 deletions
@@ -50,6 +50,7 @@ class ShorebirdCliCommandRunner extends CompletionCommandRunner<int> {
final buildCodePushApiClient =
codePushApiClientBuilder ?? ShorebirdCodePushApiClient.new;
addCommand(BuildCommand(auth: authentication, logger: _logger));
addCommand(LoginCommand(auth: authentication, logger: _logger));
addCommand(LogoutCommand(auth: authentication, logger: _logger));
addCommand(
@@ -0,0 +1,108 @@
import 'dart:io';
import 'package:args/command_runner.dart';
import 'package:mason_logger/mason_logger.dart';
import 'package:path/path.dart' as p;
import 'package:shorebird_cli/src/auth/auth.dart';
typedef RunProcess = Future<ProcessResult> Function(
String executable,
List<String> arguments, {
bool runInShell,
});
/// {@template build_command}
///
/// `shorebird build`
/// Build a new release of your application.
/// {@endtemplate}
class BuildCommand extends Command<int> {
/// {@macro build_command}
BuildCommand({
required Auth auth,
required Logger logger,
RunProcess? runProcess,
}) : _auth = auth,
_logger = logger,
_runProcess = runProcess ?? Process.run;
@override
String get description => 'Build a new release of your application.';
@override
String get name => 'build';
final Auth _auth;
final Logger _logger;
final RunProcess _runProcess;
@override
Future<int> run() async {
final session = _auth.currentSession;
if (session == null) {
_logger
..err('You must be logged in to build.')
..err("Run 'shorebird login' to log in and try again.");
return ExitCode.noUser.code;
}
final shorebirdEnginePath = p.join(
Directory.current.path,
'.shorebird',
'engine',
);
final shorebirdEngineDir = Directory(shorebirdEnginePath);
if (!shorebirdEngineDir.existsSync()) {
_logger.err(
'Shorebird engine not found. Run `shorebird run` to download it.',
);
return ExitCode.software.code;
}
final buildProgress = _logger.progress('Building release ');
try {
await _build(_runProcess, shorebirdEnginePath);
buildProgress.complete();
} on ProcessException catch (error) {
buildProgress.fail('Failed to build: ${error.message}');
return ExitCode.software.code;
}
return ExitCode.success.code;
}
Future<void> _build(RunProcess runProcess, String shorebirdEnginePath) async {
const executable = 'flutter';
final arguments = [
'build',
// This is temporary because the Shorebird engine currently
// only supports Android.
'apk',
'--release',
// This is temporary because the Shorebird engine currently
// does not support tree-shaking icons.
'--no-tree-shake-icons',
'--local-engine-src-path',
shorebirdEnginePath,
'--local-engine',
// This is temporary because the Shorebird engine currently
// only supports Android arm64.
'android_release_arm64',
];
final result = await runProcess(
executable,
arguments,
runInShell: true,
);
if (result.exitCode != ExitCode.success.code) {
throw ProcessException(
'flutter',
arguments,
result.stderr.toString(),
result.exitCode,
);
}
}
}
@@ -1,3 +1,4 @@
export 'build_command.dart';
export 'login_command.dart';
export 'logout_command.dart';
export 'publish_command.dart';
@@ -0,0 +1,101 @@
import 'dart:io';
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/auth/session.dart';
import 'package:shorebird_cli/src/commands/build_command.dart';
import 'package:test/test.dart';
class _MockAuth extends Mock implements Auth {}
class _MockLogger extends Mock implements Logger {}
class _MockProgress extends Mock implements Progress {}
class _MockProcessResult extends Mock implements ProcessResult {}
void main() {
group('build', () {
const session = Session(
apiKey: 'test-api-key',
projectId: 'test-project-id',
);
late Auth auth;
late Logger logger;
late ProcessResult processResult;
late BuildCommand buildCommand;
setUp(() {
auth = _MockAuth();
logger = _MockLogger();
processResult = _MockProcessResult();
buildCommand = BuildCommand(
auth: auth,
logger: logger,
runProcess: (executable, arguments, {bool runInShell = false}) async {
return processResult;
},
);
when(() => logger.progress(any())).thenReturn(_MockProgress());
});
test('exits with no user when not logged in', () async {
when(() => auth.currentSession).thenReturn(null);
final result = await buildCommand.run();
expect(result, equals(ExitCode.noUser.code));
verify(() => logger.err('You must be logged in to build.')).called(1);
verify(
() => logger.err("Run 'shorebird login' to log in and try again."),
).called(1);
});
test('exits with code 70 when engine is not found', () async {
when(() => auth.currentSession).thenReturn(session);
final result = await buildCommand.run();
expect(result, equals(ExitCode.software.code));
verify(
() => logger.err(
'Shorebird engine not found. Run `shorebird run` to download it.',
),
).called(1);
});
test('exits with code 70 when building fails', () async {
when(() => processResult.exitCode).thenReturn(1);
when(() => processResult.stderr).thenReturn('oops');
final tempDir = Directory.systemTemp.createTempSync();
Directory('${tempDir.path}/.shorebird/engine')
.createSync(recursive: true);
when(() => auth.currentSession).thenReturn(session);
final result = await IOOverrides.runZoned(
() async => buildCommand.run(),
getCurrentDirectory: () => tempDir,
);
expect(result, equals(ExitCode.software.code));
});
test('exits with code 0 when building succeeds', () async {
when(() => processResult.exitCode).thenReturn(ExitCode.success.code);
final tempDir = Directory.systemTemp.createTempSync();
Directory('${tempDir.path}/.shorebird/engine')
.createSync(recursive: true);
when(() => auth.currentSession).thenReturn(session);
final result = await IOOverrides.runZoned(
() async => buildCommand.run(),
getCurrentDirectory: () => tempDir,
);
expect(result, equals(ExitCode.success.code));
});
});
}
@@ -12,7 +12,7 @@ class _MockLogger extends Mock implements Logger {}
class _MockProgress extends Mock implements Progress {}
void main() {
group('LoginCommand', () {
group('login', () {
const apiKey = 'test-api-key';
const projectId = 'example';
const session = Session(apiKey: apiKey, projectId: projectId);
@@ -12,7 +12,7 @@ class _MockAuth extends Mock implements Auth {}
class _MockProgress extends Mock implements Progress {}
void main() {
group('LogoutCommand', () {
group('logout', () {
late Logger logger;
late Auth auth;
late LogoutCommand logoutCommand;