[ Widget Preview ] Improve --machine output (#175003)

This change adds the following events:

- `widget_preview.initializing`: sent immediately after the widget
previewer starts. This event includes the PID of the process starting
the widget previewer for use by tooling to explicitly kill the tool
process.
- `widget_preview.logMessage`: sent for calls to `Logger` methods when
in machine mode. This follows the same format as `daemon.logMessage`.

This change also fixes and improves testing of
`WidgetPreviewMachineAwareLogger` in `widget_preview_machine_test.dart`
as the existing test validator did not cause tests to fail correctly.

Fixes https://github.com/flutter/flutter/issues/175002
This commit is contained in:
Ben Konyi 2025-09-08 08:37:39 -04:00 committed by GitHub
parent 6ae27959de
commit e4b7bdbe22
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 91 additions and 17 deletions

View File

@ -88,6 +88,7 @@ Future<void> main(List<String> args) async {
final bool muteCommandLogging = (help || doctor) && !veryVerbose;
final bool verboseHelp = help && verbose;
final bool daemon = args.contains('daemon');
final bool widgetPreviews = args.contains(WidgetPreviewCommand.kWidgetPreview);
final bool runMachine = args.contains('--machine');
// Cache.flutterRoot must be set early because other features use it (e.g.
@ -133,6 +134,7 @@ Future<void> main(List<String> args) async {
verbose: verbose && !muteCommandLogging,
prefixedErrors: prefixedErrors,
windows: globals.platform.isWindows,
widgetPreviews: widgetPreviews,
);
},
AnsiTerminal: () {
@ -294,6 +296,7 @@ class LoggerFactory {
required bool machine,
required bool daemon,
required bool windows,
required bool widgetPreviews,
}) {
Logger logger;
if (windows) {
@ -317,6 +320,9 @@ class LoggerFactory {
if (prefixedErrors) {
logger = PrefixedErrorLogger(logger);
}
if (widgetPreviews) {
return WidgetPreviewMachineAwareLogger(logger, machine: machine, verbose: verbose);
}
if (daemon) {
return NotifyingLogger(verbose: verbose, parent: logger);
}

View File

@ -13,6 +13,7 @@ import 'package:process/process.dart';
import '../artifacts.dart';
import '../base/common.dart';
import '../base/file_system.dart';
import '../base/io.dart';
import '../base/logger.dart';
import '../base/os.dart';
import '../base/platform.dart';
@ -28,7 +29,6 @@ import '../isolated/resident_web_runner.dart';
import '../project.dart';
import '../resident_runner.dart';
import '../runner/flutter_command.dart';
import '../runner/flutter_command_runner.dart';
import '../web/web_device.dart';
import '../widget_preview/analytics.dart';
import '../widget_preview/dependency_graph.dart';
@ -77,7 +77,8 @@ class WidgetPreviewCommand extends FlutterCommand {
String get description => 'Manage the widget preview environment.';
@override
String get name => 'widget-preview';
String get name => kWidgetPreview;
static const kWidgetPreview = 'widget-preview';
@override
String get category => FlutterCommandCategory.tools;
@ -130,7 +131,7 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C
required this.processManager,
required this.artifacts,
@visibleForTesting WidgetPreviewDtdServices? dtdServicesOverride,
}) : logger = WidgetPreviewMachineAwareLogger(logger) {
}) : _logger = logger {
if (dtdServicesOverride != null) {
_dtdService = dtdServicesOverride;
}
@ -200,7 +201,8 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C
final FileSystem fs;
@override
final WidgetPreviewMachineAwareLogger logger;
WidgetPreviewMachineAwareLogger get logger => _logger as WidgetPreviewMachineAwareLogger;
final Logger _logger;
@override
final FlutterProjectFactory projectFactory;
@ -259,17 +261,17 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C
@override
Future<FlutterCommandResult> runCommand() async {
assert(_logger is WidgetPreviewMachineAwareLogger);
// Start the timer tracking how long it takes to launch the preview environment.
previewAnalytics.initializeLaunchStopwatch();
logger.sendInitializingEvent();
final String? customPreviewScaffoldOutput = stringArg(kWidgetPreviewScaffoldOutputDir);
final Directory widgetPreviewScaffold = customPreviewScaffoldOutput != null
? 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 =
@ -479,7 +481,7 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C
);
unawaited(_widgetPreviewApp!.run(appStartedCompleter: appStarted));
await appStarted.future;
logger.sendEvent('started', {'url': flutterDevice.devFS!.baseUri.toString()});
logger.sendStartedEvent(applicationUrl: flutterDevice.devFS!.baseUri!);
}
} on Exception catch (error) {
throwToolExit(error.toString());
@ -531,9 +533,10 @@ final class WidgetPreviewCleanCommand extends WidgetPreviewSubCommandBase {
/// 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);
WidgetPreviewMachineAwareLogger(super.delegate, {required this.machine, required this.verbose});
var machine = false;
final bool machine;
final bool verbose;
@override
void printError(
@ -546,6 +549,11 @@ final class WidgetPreviewMachineAwareLogger extends DelegatingLogger {
bool? wrap,
}) {
if (machine) {
sendEvent('logMessage', <String, Object?>{
'level': 'error',
'message': message,
'stackTrace': ?stackTrace?.toString(),
});
return;
}
super.printError(
@ -570,6 +578,7 @@ final class WidgetPreviewMachineAwareLogger extends DelegatingLogger {
bool fatal = true,
}) {
if (machine) {
sendEvent('logMessage', <String, Object?>{'level': 'warning', 'message': message});
return;
}
super.printWarning(
@ -594,6 +603,7 @@ final class WidgetPreviewMachineAwareLogger extends DelegatingLogger {
bool? wrap,
}) {
if (machine) {
sendEvent('logMessage', <String, Object?>{'level': 'status', 'message': message});
return;
}
super.printStatus(
@ -617,12 +627,27 @@ final class WidgetPreviewMachineAwareLogger extends DelegatingLogger {
@override
void printTrace(String message) {
if (!verbose) {
return;
}
if (machine) {
sendEvent('logMessage', <String, Object?>{'level': 'trace', 'message': message});
return;
}
super.printTrace(message);
}
/// Notifies tooling that the widget previewer is initializing.
void sendInitializingEvent() {
sendEvent('initializing', {'pid': pid});
}
/// Notifies tooling that the widget previewer has started and is being
/// served at [applicationUrl].
void sendStartedEvent({required Uri applicationUrl}) {
sendEvent('started', {'url': applicationUrl.toString()});
}
@override
void sendEvent(String name, [Map<String, dynamic>? args]) {
if (!machine) {
@ -642,6 +667,7 @@ final class WidgetPreviewMachineAwareLogger extends DelegatingLogger {
int progressIndicatorPadding = kDefaultStatusPadding,
}) {
if (machine) {
printStatus(message);
return SilentStatus(stopwatch: Stopwatch());
}
return super.startProgress(

View File

@ -109,7 +109,7 @@ void main() {
await ensureFlutterToolsSnapshot();
loggingProcessManager = LoggingProcessManager();
shutdownHooks = ShutdownHooks();
logger = BufferLogger.test();
logger = WidgetPreviewMachineAwareLogger(BufferLogger.test(), machine: false, verbose: false);
fs = LocalFileSystem.test(signals: Signals.test());
botDetector = const FakeBotDetector(false);
tempDir = fs.systemTempDirectory.createTempSync('flutter_tools_create_test.');
@ -190,7 +190,7 @@ void main() {
void expectSinglePreviewLaunchTimingEvent() => expectNPreviewLaunchTimingEvents(1);
void expectDeviceSelected(Device device) {
final bufferLogger = logger as BufferLogger;
final BufferLogger bufferLogger = asLogger<BufferLogger>(logger);
expect(
bufferLogger.statusText,
contains('Launching the Widget Preview Scaffold on ${device.displayName}...'),

View File

@ -12,6 +12,7 @@ import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/base/terminal.dart';
import 'package:flutter_tools/src/commands/daemon.dart';
import 'package:flutter_tools/src/commands/widget_preview.dart';
import 'package:test/fake.dart';
import '../../src/common.dart';
@ -37,6 +38,7 @@ void main() {
prefixedErrors: false,
machine: false,
daemon: false,
widgetPreviews: false,
windows: false,
),
isA<StdoutLogger>(),
@ -47,6 +49,7 @@ void main() {
prefixedErrors: false,
machine: false,
daemon: false,
widgetPreviews: false,
windows: true,
),
isA<WindowsStdoutLogger>(),
@ -57,6 +60,7 @@ void main() {
prefixedErrors: false,
machine: false,
daemon: false,
widgetPreviews: false,
windows: true,
),
isA<VerboseLogger>(),
@ -67,6 +71,7 @@ void main() {
prefixedErrors: false,
machine: false,
daemon: false,
widgetPreviews: false,
windows: false,
),
isA<VerboseLogger>(),
@ -77,6 +82,7 @@ void main() {
prefixedErrors: true,
machine: false,
daemon: false,
widgetPreviews: false,
windows: false,
),
isA<PrefixedErrorLogger>(),
@ -87,6 +93,7 @@ void main() {
prefixedErrors: false,
machine: false,
daemon: true,
widgetPreviews: false,
windows: false,
),
isA<NotifyingLogger>(),
@ -97,10 +104,32 @@ void main() {
prefixedErrors: false,
machine: true,
daemon: false,
widgetPreviews: false,
windows: false,
),
isA<MachineOutputLogger>(),
);
for (final verbose in [false, true]) {
for (final machine in [false, true]) {
for (final windows in [false, true]) {
expect(
loggerFactory.createLogger(
verbose: verbose,
prefixedErrors: false,
machine: machine,
daemon: false,
widgetPreviews: true,
windows: windows,
),
isA<WidgetPreviewMachineAwareLogger>(),
reason:
'Did not produce WidgetPreviewMachineAwareLogger for '
'verbose: $verbose machine: $machine windows: $windows.',
);
}
}
}
});
testWithoutContext(

View File

@ -604,6 +604,7 @@ void main() {
machine: true,
verbose: false,
prefixedErrors: false,
widgetPreviews: false,
windows: globals.platform.isWindows,
);
},

View File

@ -16,20 +16,28 @@ 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});
typedef ExpectedEvent = ({String event, FutureOr<bool> Function(Map<String, Object?>)? validator});
final launchEvents = <ExpectedEvent>[
(
event: 'widget_preview.initializing',
validator: (Map<String, Object?> params) {
return params.containsKey('pid');
},
),
(
event: 'widget_preview.started',
validator: (Map<String, Object?> params) async {
if (params case {'uri': final String uri}) {
if (params case {'url': final String uri}) {
try {
final Response response = await get(Uri.parse(uri));
expect(response.statusCode, HttpStatus.ok, reason: 'Failed to retrieve widget previewer');
return response.statusCode == HttpStatus.ok;
// ignore: avoid_catches_without_on_clauses
} catch (e) {
fail('Failed to access widget previewer: $e');
printOnFailure('Failed to access widget previewer: $e');
}
}
return false;
},
),
];
@ -82,7 +90,11 @@ void main() {
if (event case [final Map<String, Object?> eventObject]) {
final ExpectedEvent expectation = expectedEvents[nextExpectationIndex];
if (expectation.event == eventObject['event']) {
await expectation.validator?.call(eventObject);
expect(
await expectation.validator?.call(eventObject['params']! as Map<String, Object?>),
true,
reason: 'Validator[$nextExpectationIndex] failed.',
);
++nextExpectationIndex;
}
}