From 11cef956b782480c982a2d7895bdeb45ae890fee Mon Sep 17 00:00:00 2001 From: Felix Angelov Date: Mon, 16 Dec 2024 14:41:57 -0600 Subject: [PATCH] feat(shorebird_cli): `shorebird preview` stream logs on MacOS (#2688) --- packages/shorebird_cli/bin/shorebird.dart | 1 + .../lib/src/commands/preview_command.dart | 13 +++-- .../lib/src/executables/executables.dart | 1 + .../lib/src/executables/open.dart | 39 ++++++++++++++ .../src/commands/preview_command_test.dart | 23 +++----- .../test/src/executables/open_test.dart | 54 +++++++++++++++++++ packages/shorebird_cli/test/src/mocks.dart | 2 + 7 files changed, 113 insertions(+), 20 deletions(-) create mode 100644 packages/shorebird_cli/lib/src/executables/open.dart create mode 100644 packages/shorebird_cli/test/src/executables/open_test.dart diff --git a/packages/shorebird_cli/bin/shorebird.dart b/packages/shorebird_cli/bin/shorebird.dart index d67949a2..1021d8ac 100644 --- a/packages/shorebird_cli/bin/shorebird.dart +++ b/packages/shorebird_cli/bin/shorebird.dart @@ -75,6 +75,7 @@ Command: shorebird ${args.join(' ')} javaRef, loggerRef, networkCheckerRef, + openRef, osInterfaceRef, patchExecutableRef, patchDiffCheckerRef, diff --git a/packages/shorebird_cli/lib/src/commands/preview_command.dart b/packages/shorebird_cli/lib/src/commands/preview_command.dart index 82c10cf2..b625fed8 100644 --- a/packages/shorebird_cli/lib/src/commands/preview_command.dart +++ b/packages/shorebird_cli/lib/src/commands/preview_command.dart @@ -21,7 +21,6 @@ import 'package:shorebird_cli/src/logging/logging.dart'; import 'package:shorebird_cli/src/platform.dart'; import 'package:shorebird_cli/src/shorebird_command.dart'; import 'package:shorebird_cli/src/shorebird_env.dart'; -import 'package:shorebird_cli/src/shorebird_process.dart'; import 'package:shorebird_cli/src/shorebird_validator.dart'; import 'package:shorebird_cli/src/third_party/flutter_tools/lib/flutter_tools.dart'; import 'package:shorebird_code_push_client/shorebird_code_push_client.dart'; @@ -339,9 +338,15 @@ class PreviewCommand extends ShorebirdCommand { } } - // TODO(felangel): wrap `open` and stream logs. - final proc = await process.start('open', ['-n', appDirectory.path]); - return proc.exitCode; + final logs = await open.newApplication(path: appDirectory.path); + final completer = Completer(); + + logs.listen( + (log) => logger.info(utf8.decode(log)), + onDone: completer.complete, + ); + + return completer.future.then((_) => ExitCode.success.code); } Future installAndLaunchAndroid({ diff --git a/packages/shorebird_cli/lib/src/executables/executables.dart b/packages/shorebird_cli/lib/src/executables/executables.dart index 9814dc8e..edde68ba 100644 --- a/packages/shorebird_cli/lib/src/executables/executables.dart +++ b/packages/shorebird_cli/lib/src/executables/executables.dart @@ -10,6 +10,7 @@ export 'gradlew.dart'; export 'idevicesyslog.dart'; export 'ios_deploy.dart'; export 'java.dart'; +export 'open.dart'; export 'patch_executable.dart'; export 'shorebird_tools.dart'; export 'xcodebuild.dart'; diff --git a/packages/shorebird_cli/lib/src/executables/open.dart b/packages/shorebird_cli/lib/src/executables/open.dart new file mode 100644 index 00000000..1f6eae8b --- /dev/null +++ b/packages/shorebird_cli/lib/src/executables/open.dart @@ -0,0 +1,39 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:path/path.dart' as p; +import 'package:scoped_deps/scoped_deps.dart'; +import 'package:shorebird_cli/src/shorebird_process.dart'; + +/// A reference to a [Open] instance. +final openRef = create(Open.new); + +/// The [Open] instance available in the current zone. +Open get open => read(openRef); + +/// A wrapper around the `open` command. +/// https://ss64.com/mac/open.html +class Open { + /// Opens a new application at the provided [path] and streams the stdout and + /// stderr. + Future>> newApplication({required String path}) async { + final tmp = Directory.systemTemp.createTempSync(); + final stdout = File(p.join(tmp.path, 'stdout.log'))..createSync(); + await process.start( + 'open', + [ + '-n', + path, + '--stdout=${stdout.path}', + '--stderr=${stdout.path}', + ], + ); + + final stdoutProcess = await process.start( + 'tail', + ['-fn+1', stdout.path], + ); + + return stdoutProcess.stdout; + } +} diff --git a/packages/shorebird_cli/test/src/commands/preview_command_test.dart b/packages/shorebird_cli/test/src/commands/preview_command_test.dart index 8cce2d0e..fe510ec4 100644 --- a/packages/shorebird_cli/test/src/commands/preview_command_test.dart +++ b/packages/shorebird_cli/test/src/commands/preview_command_test.dart @@ -22,7 +22,6 @@ import 'package:shorebird_cli/src/http_client/http_client.dart'; import 'package:shorebird_cli/src/logging/logging.dart'; import 'package:shorebird_cli/src/platform.dart'; import 'package:shorebird_cli/src/shorebird_env.dart'; -import 'package:shorebird_cli/src/shorebird_process.dart'; import 'package:shorebird_cli/src/shorebird_validator.dart'; import 'package:shorebird_code_push_client/shorebird_code_push_client.dart'; import 'package:test/test.dart'; @@ -1518,8 +1517,7 @@ channel: ${DeploymentTrack.staging.channel} const releasePlatform = ReleasePlatform.macos; late Ditto ditto; - late ShorebirdProcess shorebirdProcess; - late Process process; + late Open open; R runWithOverrides(R Function() body) { return HttpOverrides.runZoned( @@ -1537,7 +1535,7 @@ channel: ${DeploymentTrack.staging.channel} httpClientRef.overrideWith(() => httpClient), loggerRef.overrideWith(() => logger), platformRef.overrideWith(() => platform), - processRef.overrideWith(() => shorebirdProcess), + openRef.overrideWith(() => open), shorebirdEnvRef.overrideWith(() => shorebirdEnv), shorebirdValidatorRef.overrideWith(() => shorebirdValidator), }, @@ -1547,8 +1545,7 @@ channel: ${DeploymentTrack.staging.channel} setUp(() { ditto = MockDitto(); - shorebirdProcess = MockShorebirdProcess(); - process = MockProcess(); + open = MockOpen(); when(() => releaseArtifact.id).thenReturn(macosArtifactId); when(() => argResults['platform']).thenReturn(releasePlatform.name); @@ -1572,9 +1569,9 @@ channel: ${DeploymentTrack.staging.channel} }); when(() => releaseArtifact.url).thenReturn(releaseArtifactUrl); when(() => platform.isMacOS).thenReturn(true); - when(() => shorebirdProcess.start(any(), any())).thenAnswer( - (_) async => process, - ); + when( + () => open.newApplication(path: any(named: 'path')), + ).thenAnswer((_) async => Stream.value(utf8.encode('hello world'))); }); group('when querying for release artifact fails', () { @@ -1638,16 +1635,10 @@ channel: ${DeploymentTrack.staging.channel} }); group('when process completes with exit code 0', () { - setUp(() { - when(() => process.exitCode).thenAnswer((_) async => 0); - }); - test('completes successfully', () async { final result = await runWithOverrides(command.run); expect(result, equals(ExitCode.success.code)); - verify( - () => shorebirdProcess.start('open', any()), - ).called(1); + verify(() => logger.info('hello world')).called(1); }); }); }); diff --git a/packages/shorebird_cli/test/src/executables/open_test.dart b/packages/shorebird_cli/test/src/executables/open_test.dart new file mode 100644 index 00000000..122314dc --- /dev/null +++ b/packages/shorebird_cli/test/src/executables/open_test.dart @@ -0,0 +1,54 @@ +import 'dart:convert'; + +import 'package:mocktail/mocktail.dart'; +import 'package:scoped_deps/scoped_deps.dart'; +import 'package:shorebird_cli/src/executables/executables.dart'; +import 'package:shorebird_cli/src/shorebird_process.dart'; +import 'package:test/test.dart'; + +import '../mocks.dart'; + +void main() { + group(Open, () { + late Open open; + late ShorebirdProcess process; + + R runWithOverrides(R Function() body) { + return runScoped( + body, + values: { + processRef.overrideWith(() => process), + }, + ); + } + + setUp(() { + process = MockShorebirdProcess(); + open = Open(); + }); + + group('newApplication', () { + test('executes correct command and streams logs', () async { + final openProcess = MockProcess(); + final tailProcess = MockProcess(); + + when(() => process.start('open', any())).thenAnswer((_) async { + return openProcess; + }); + when(() => process.start('tail', any())).thenAnswer((_) async { + return tailProcess; + }); + + when(() => tailProcess.stdout).thenAnswer( + (_) => Stream.fromIterable([utf8.encode('hello world') as List]), + ); + + final stream = await runWithOverrides( + () => open.newApplication(path: 'test'), + ); + + expect(stream, emits(utf8.encode('hello world'))); + }); + }); + }); +} diff --git a/packages/shorebird_cli/test/src/mocks.dart b/packages/shorebird_cli/test/src/mocks.dart index 76c96783..4038b394 100644 --- a/packages/shorebird_cli/test/src/mocks.dart +++ b/packages/shorebird_cli/test/src/mocks.dart @@ -117,6 +117,8 @@ class MockJwtPayload extends Mock implements JwtPayload {} class MockNetworkChecker extends Mock implements NetworkChecker {} +class MockOpen extends Mock implements Open {} + class MockOperatingSystemInterface extends Mock implements OperatingSystemInterface {}