mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
[ Widget Preview ] Add --machine mode (#173654)
Currently only outputs a single event with information about where the
widget preview environment is served from:
`[{"event":"widget_preview.started","params":{"url":"http://localhost:61383"}}]`
Fixes https://github.com/flutter/flutter/issues/173545
This commit is contained in:
parent
9e99953a4e
commit
f8e8a8dce9
@ -88,10 +88,7 @@ Future<void> main(List<String> args) async {
|
||||
final bool daemon = args.contains('daemon');
|
||||
final bool runMachine =
|
||||
(args.contains('--machine') && args.contains('run')) ||
|
||||
(args.contains('--machine') && args.contains('attach')) ||
|
||||
// `flutter widget-preview start` starts an application that requires a logger
|
||||
// to be setup for machine mode.
|
||||
(args.contains('widget-preview') && args.contains('start'));
|
||||
(args.contains('--machine') && args.contains('attach'));
|
||||
|
||||
// Cache.flutterRoot must be set early because other features use it (e.g.
|
||||
// enginePath's initializer uses it). This can only work with the real
|
||||
|
||||
@ -16,9 +16,11 @@ import '../base/logger.dart';
|
||||
import '../base/os.dart';
|
||||
import '../base/platform.dart';
|
||||
import '../base/process.dart';
|
||||
import '../base/terminal.dart';
|
||||
import '../build_info.dart';
|
||||
import '../bundle.dart' as bundle;
|
||||
import '../cache.dart';
|
||||
import '../convert.dart';
|
||||
import '../device.dart';
|
||||
import '../globals.dart' as globals;
|
||||
import '../isolated/resident_web_runner.dart';
|
||||
@ -116,7 +118,7 @@ abstract base class WidgetPreviewSubCommandBase extends FlutterCommand {
|
||||
final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with CreateBase {
|
||||
WidgetPreviewStartCommand({
|
||||
this.verbose = false,
|
||||
required this.logger,
|
||||
required Logger logger,
|
||||
required this.fs,
|
||||
required this.projectFactory,
|
||||
required this.cache,
|
||||
@ -126,11 +128,12 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C
|
||||
required this.processManager,
|
||||
required this.artifacts,
|
||||
@visibleForTesting WidgetPreviewDtdServices? dtdServicesOverride,
|
||||
}) {
|
||||
}) : logger = WidgetPreviewMachineAwareLogger(logger) {
|
||||
if (dtdServicesOverride != null) {
|
||||
_dtdService = dtdServicesOverride;
|
||||
}
|
||||
addPubOptions();
|
||||
addMachineOutputFlag(verboseHelp: verbose);
|
||||
argParser
|
||||
..addFlag(
|
||||
kWebServer,
|
||||
@ -189,7 +192,7 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C
|
||||
final FileSystem fs;
|
||||
|
||||
@override
|
||||
final Logger logger;
|
||||
final WidgetPreviewMachineAwareLogger logger;
|
||||
|
||||
@override
|
||||
final FlutterProjectFactory projectFactory;
|
||||
@ -255,6 +258,9 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C
|
||||
? fs.directory(customPreviewScaffoldOutput)
|
||||
: rootProject.widgetPreviewScaffold;
|
||||
|
||||
final bool machine = boolArg(FlutterGlobalOptions.kMachineFlag);
|
||||
logger.machine = machine;
|
||||
|
||||
// Check to see if a preview scaffold has already been generated. If not,
|
||||
// generate one.
|
||||
final bool generateScaffoldProject =
|
||||
@ -444,6 +450,7 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C
|
||||
);
|
||||
unawaited(_widgetPreviewApp!.run(appStartedCompleter: appStarted));
|
||||
await appStarted.future;
|
||||
logger.sendEvent('started', {'url': flutterDevice.devFS!.baseUri.toString()});
|
||||
}
|
||||
} on Exception catch (error) {
|
||||
throwToolExit(error.toString());
|
||||
@ -491,3 +498,145 @@ final class WidgetPreviewCleanCommand extends WidgetPreviewSubCommandBase {
|
||||
return FlutterCommandResult.success();
|
||||
}
|
||||
}
|
||||
|
||||
/// A custom logger for the widget-preview commands that disables non-event output to stdio when
|
||||
/// machine mode is enabled.
|
||||
final class WidgetPreviewMachineAwareLogger extends DelegatingLogger {
|
||||
WidgetPreviewMachineAwareLogger(super.delegate);
|
||||
|
||||
var machine = false;
|
||||
|
||||
@override
|
||||
void printError(
|
||||
String message, {
|
||||
StackTrace? stackTrace,
|
||||
bool? emphasis,
|
||||
TerminalColor? color,
|
||||
int? indent,
|
||||
int? hangingIndent,
|
||||
bool? wrap,
|
||||
}) {
|
||||
if (machine) {
|
||||
return;
|
||||
}
|
||||
super.printError(
|
||||
message,
|
||||
stackTrace: stackTrace,
|
||||
emphasis: emphasis,
|
||||
color: color,
|
||||
indent: indent,
|
||||
hangingIndent: hangingIndent,
|
||||
wrap: wrap,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void printWarning(
|
||||
String message, {
|
||||
bool? emphasis,
|
||||
TerminalColor? color,
|
||||
int? indent,
|
||||
int? hangingIndent,
|
||||
bool? wrap,
|
||||
bool fatal = true,
|
||||
}) {
|
||||
if (machine) {
|
||||
return;
|
||||
}
|
||||
super.printWarning(
|
||||
message,
|
||||
emphasis: emphasis,
|
||||
color: color,
|
||||
indent: indent,
|
||||
hangingIndent: hangingIndent,
|
||||
wrap: wrap,
|
||||
fatal: fatal,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void printStatus(
|
||||
String message, {
|
||||
bool? emphasis,
|
||||
TerminalColor? color,
|
||||
bool? newline,
|
||||
int? indent,
|
||||
int? hangingIndent,
|
||||
bool? wrap,
|
||||
}) {
|
||||
if (machine) {
|
||||
return;
|
||||
}
|
||||
super.printStatus(
|
||||
message,
|
||||
emphasis: emphasis,
|
||||
color: color,
|
||||
newline: newline,
|
||||
indent: indent,
|
||||
hangingIndent: hangingIndent,
|
||||
wrap: wrap,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void printBox(String message, {String? title}) {
|
||||
if (machine) {
|
||||
return;
|
||||
}
|
||||
super.printBox(message, title: title);
|
||||
}
|
||||
|
||||
@override
|
||||
void printTrace(String message) {
|
||||
if (machine) {
|
||||
return;
|
||||
}
|
||||
super.printTrace(message);
|
||||
}
|
||||
|
||||
@override
|
||||
void sendEvent(String name, [Map<String, dynamic>? args]) {
|
||||
if (!machine) {
|
||||
return;
|
||||
}
|
||||
super.printStatus(
|
||||
json.encode([
|
||||
{'event': 'widget_preview.$name', 'params': ?args},
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Status startProgress(
|
||||
String message, {
|
||||
String? progressId,
|
||||
int progressIndicatorPadding = kDefaultStatusPadding,
|
||||
}) {
|
||||
if (machine) {
|
||||
return SilentStatus(stopwatch: Stopwatch());
|
||||
}
|
||||
return super.startProgress(
|
||||
message,
|
||||
progressId: progressId,
|
||||
progressIndicatorPadding: progressIndicatorPadding,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Status startSpinner({
|
||||
VoidCallback? onFinish,
|
||||
Duration? timeout,
|
||||
SlowWarningCallback? slowWarningCallback,
|
||||
TerminalColor? warningColor,
|
||||
}) {
|
||||
if (machine) {
|
||||
return SilentStatus(stopwatch: Stopwatch());
|
||||
}
|
||||
return super.startSpinner(
|
||||
onFinish: onFinish,
|
||||
timeout: timeout,
|
||||
slowWarningCallback: slowWarningCallback,
|
||||
warningColor: warningColor,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,123 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:file/file.dart';
|
||||
import 'package:flutter_tools/src/base/io.dart';
|
||||
import 'package:flutter_tools/src/commands/widget_preview.dart';
|
||||
import 'package:flutter_tools/src/widget_preview/dtd_services.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:process/process.dart';
|
||||
|
||||
import '../src/common.dart';
|
||||
import 'test_data/basic_project.dart';
|
||||
import 'test_utils.dart';
|
||||
|
||||
typedef ExpectedEvent = ({String event, FutureOr<void> Function(Map<String, Object?>)? validator});
|
||||
|
||||
final launchEvents = <ExpectedEvent>[
|
||||
(
|
||||
event: 'widget_preview.started',
|
||||
validator: (Map<String, Object?> params) async {
|
||||
if (params case {'uri': final String uri}) {
|
||||
try {
|
||||
final Response response = await get(Uri.parse(uri));
|
||||
expect(response.statusCode, HttpStatus.ok, reason: 'Failed to retrieve widget previewer');
|
||||
} catch (e) {
|
||||
fail('Failed to access widget previewer: $e');
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
];
|
||||
|
||||
void main() {
|
||||
late Directory tempDir;
|
||||
Process? process;
|
||||
DtdLauncher? dtdLauncher;
|
||||
final project = BasicProject();
|
||||
const ProcessManager processManager = LocalProcessManager();
|
||||
|
||||
setUp(() async {
|
||||
tempDir = createResolvedTempDirectorySync('widget_preview_test.');
|
||||
await project.setUpIn(tempDir);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
process?.kill();
|
||||
process = null;
|
||||
await dtdLauncher?.dispose();
|
||||
dtdLauncher = null;
|
||||
tryToDelete(tempDir);
|
||||
});
|
||||
|
||||
Future<void> runWidgetPreviewMachineMode({
|
||||
required List<ExpectedEvent> expectedEvents,
|
||||
bool useWebServer = false,
|
||||
}) async {
|
||||
expect(expectedEvents, isNotEmpty);
|
||||
process = await processManager.start(<String>[
|
||||
flutterBin,
|
||||
'widget-preview',
|
||||
'start',
|
||||
'--machine',
|
||||
'--${WidgetPreviewStartCommand.kHeadless}',
|
||||
if (useWebServer) '--${WidgetPreviewStartCommand.kWebServer}',
|
||||
], workingDirectory: tempDir.path);
|
||||
|
||||
final completer = Completer<void>();
|
||||
var nextExpectationIndex = 0;
|
||||
process!.stdout.transform(utf8.decoder).transform(const LineSplitter()).listen((
|
||||
String message,
|
||||
) async {
|
||||
printOnFailure('STDOUT: $message');
|
||||
if (completer.isCompleted) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
final Object? event = json.decode(message);
|
||||
if (event case [final Map<String, Object?> eventObject]) {
|
||||
final ExpectedEvent expectation = expectedEvents[nextExpectationIndex];
|
||||
if (expectation.event == eventObject['event']) {
|
||||
await expectation.validator?.call(eventObject);
|
||||
++nextExpectationIndex;
|
||||
}
|
||||
}
|
||||
if (nextExpectationIndex == expectedEvents.length) {
|
||||
completer.complete();
|
||||
}
|
||||
} on FormatException {
|
||||
// Do nothing.
|
||||
}
|
||||
});
|
||||
|
||||
process!.stderr.transform(utf8.decoder).transform(const LineSplitter()).listen((String msg) {
|
||||
printOnFailure('STDERR: $msg');
|
||||
});
|
||||
|
||||
unawaited(
|
||||
process!.exitCode.then((int exitCode) {
|
||||
if (completer.isCompleted) {
|
||||
return;
|
||||
}
|
||||
completer.completeError(
|
||||
TestFailure('The widget previewer exited unexpectedly (exit code: $exitCode)'),
|
||||
);
|
||||
}),
|
||||
);
|
||||
await completer.future;
|
||||
}
|
||||
|
||||
group('flutter widget-preview start --machine', () {
|
||||
testWithoutContext('launches in browser', () async {
|
||||
await runWidgetPreviewMachineMode(expectedEvents: launchEvents);
|
||||
});
|
||||
|
||||
testWithoutContext('launches web server', () async {
|
||||
await runWidgetPreviewMachineMode(expectedEvents: launchEvents, useWebServer: true);
|
||||
});
|
||||
});
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user