Only write the pid-file while listening to SIGUSR signals. (#74533)
This commit is contained in:
parent
1b44133322
commit
b1cc48748d
@ -2,19 +2,11 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter_driver/driver_extension.dart';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
String log = '';
|
||||
|
||||
void main() {
|
||||
enableFlutterDriverExtension(handler: (String message) async {
|
||||
log = 'log:';
|
||||
await WidgetsBinding.instance.reassembleApplication();
|
||||
return log;
|
||||
});
|
||||
print('called main');
|
||||
runApp(const MaterialApp(home: Test()));
|
||||
}
|
||||
|
||||
@ -22,9 +14,7 @@ class Test extends SingleChildRenderObjectWidget {
|
||||
const Test({ Key key }) : super(key: key);
|
||||
|
||||
@override
|
||||
RenderTest createRenderObject(BuildContext context) {
|
||||
return RenderTest();
|
||||
}
|
||||
RenderTest createRenderObject(BuildContext context) => RenderTest();
|
||||
}
|
||||
|
||||
class RenderTest extends RenderProxyBox {
|
||||
@ -33,11 +23,17 @@ class RenderTest extends RenderProxyBox {
|
||||
@override
|
||||
void debugPaintSize(PaintingContext context, Offset offset) {
|
||||
super.debugPaintSize(context, offset);
|
||||
log += ' debugPaintSize';
|
||||
print('called debugPaintSize');
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(PaintingContext context, Offset offset) {
|
||||
log += ' paint';
|
||||
print('called paint');
|
||||
}
|
||||
|
||||
@override
|
||||
void reassemble() {
|
||||
print('called reassemble');
|
||||
super.reassemble();
|
||||
}
|
||||
}
|
||||
|
41
dev/integration_tests/ui/lib/overflow.dart
Normal file
41
dev/integration_tests/ui/lib/overflow.dart
Normal file
@ -0,0 +1,41 @@
|
||||
// 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 'package:flutter/material.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const MaterialApp(home: Test()));
|
||||
}
|
||||
|
||||
class Test extends StatefulWidget {
|
||||
const Test({ Key key }) : super(key: key);
|
||||
|
||||
@override
|
||||
State<Test> createState() => _TestState();
|
||||
}
|
||||
|
||||
class _TestState extends State<Test> {
|
||||
bool _triggered = false;
|
||||
|
||||
@override
|
||||
void reassemble() {
|
||||
_triggered = true;
|
||||
super.reassemble();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!_triggered)
|
||||
return const SizedBox.shrink();
|
||||
return Row(children: const <Widget>[
|
||||
SizedBox(width: 10000.0),
|
||||
SizedBox(width: 10000.0),
|
||||
SizedBox(width: 10000.0),
|
||||
SizedBox(width: 10000.0),
|
||||
SizedBox(width: 10000.0),
|
||||
SizedBox(width: 10000.0),
|
||||
SizedBox(width: 10000.0),
|
||||
]);
|
||||
}
|
||||
}
|
@ -52,6 +52,7 @@ import 'package:meta/meta.dart';
|
||||
import '../globals.dart' as globals;
|
||||
import 'async_guard.dart';
|
||||
import 'context.dart';
|
||||
import 'file_system.dart';
|
||||
import 'process.dart';
|
||||
|
||||
export 'dart:io'
|
||||
@ -364,34 +365,60 @@ class Stdio {
|
||||
Future<void> addStderrStream(Stream<List<int>> stream) => stderr.addStream(stream);
|
||||
}
|
||||
|
||||
// TODO(zra): Move pid and writePidFile into `ProcessInfo`.
|
||||
void writePidFile(String pidFile) {
|
||||
if (pidFile != null) {
|
||||
// Write our pid to the file.
|
||||
globals.fs.file(pidFile).writeAsStringSync(io.pid.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/// An overridable version of io.ProcessInfo.
|
||||
abstract class ProcessInfo {
|
||||
factory ProcessInfo() => _DefaultProcessInfo();
|
||||
factory ProcessInfo() => _DefaultProcessInfo(globals.fs);
|
||||
factory ProcessInfo.test(FileSystem fs) => _TestProcessInfo(fs);
|
||||
|
||||
static ProcessInfo get instance => context.get<ProcessInfo>();
|
||||
|
||||
int get currentRss;
|
||||
|
||||
int get maxRss;
|
||||
|
||||
File writePidFile(String pidFile);
|
||||
}
|
||||
|
||||
ProcessInfo get processInfo => ProcessInfo.instance;
|
||||
|
||||
/// The default implementation of [ProcessInfo], which uses [io.ProcessInfo].
|
||||
class _DefaultProcessInfo implements ProcessInfo {
|
||||
_DefaultProcessInfo(this._fileSystem);
|
||||
|
||||
final FileSystem _fileSystem;
|
||||
|
||||
@override
|
||||
int get currentRss => io.ProcessInfo.currentRss;
|
||||
|
||||
@override
|
||||
int get maxRss => io.ProcessInfo.maxRss;
|
||||
|
||||
@override
|
||||
File writePidFile(String pidFile) {
|
||||
assert(pidFile != null);
|
||||
return _fileSystem.file(pidFile)
|
||||
..writeAsStringSync(io.pid.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/// The test version of [ProcessInfo].
|
||||
class _TestProcessInfo implements ProcessInfo {
|
||||
_TestProcessInfo(this._fileSystem);
|
||||
|
||||
final FileSystem _fileSystem;
|
||||
|
||||
@override
|
||||
int currentRss = 1000;
|
||||
|
||||
@override
|
||||
int maxRss = 2000;
|
||||
|
||||
@override
|
||||
File writePidFile(String pidFile) {
|
||||
assert(pidFile != null);
|
||||
return _fileSystem.file(pidFile)
|
||||
..writeAsStringSync('12345');
|
||||
}
|
||||
}
|
||||
|
||||
/// The return type for [listNetworkInterfaces].
|
||||
|
@ -90,18 +90,26 @@ class AttachCommand extends FlutterCommand {
|
||||
'This parameter is case-insensitive.',
|
||||
)..addOption(
|
||||
'pid-file',
|
||||
help: 'Specify a file to write the process id to. '
|
||||
help: 'Specify a file to write the process ID to. '
|
||||
'You can send SIGUSR1 to trigger a hot reload '
|
||||
'and SIGUSR2 to trigger a hot restart.',
|
||||
'and SIGUSR2 to trigger a hot restart. '
|
||||
'The file is created when the signal handlers '
|
||||
'are hooked and deleted when they are removed.',
|
||||
)..addFlag(
|
||||
'report-ready',
|
||||
help: 'Print "ready" to the console after handling a keyboard command.\n'
|
||||
'This is primarily useful for tests and other automation, but consider '
|
||||
'using --machine instead.',
|
||||
hide: !verboseHelp,
|
||||
)..addOption(
|
||||
'project-root',
|
||||
hide: !verboseHelp,
|
||||
help: 'Normally used only in run target',
|
||||
help: 'Normally used only in run target.',
|
||||
)..addFlag('machine',
|
||||
hide: !verboseHelp,
|
||||
negatable: false,
|
||||
help: 'Handle machine structured JSON command input and provide output '
|
||||
'and progress in machine friendly format.',
|
||||
'and progress in machine-friendly format.',
|
||||
);
|
||||
usesTrackWidgetCreation(verboseHelp: verboseHelp);
|
||||
addDdsOptions(verboseHelp: verboseHelp);
|
||||
@ -200,8 +208,6 @@ known, it can be explicitly provided to attach via the command-line, e.g.
|
||||
Future<FlutterCommandResult> runCommand() async {
|
||||
await _validateArguments();
|
||||
|
||||
writePidFile(stringArg('pid-file'));
|
||||
|
||||
final Device device = await findTargetDevice();
|
||||
|
||||
final Artifacts overrideArtifacts = device.artifactOverrides ?? globals.artifacts;
|
||||
@ -362,9 +368,12 @@ known, it can be explicitly provided to attach via the command-line, e.g.
|
||||
logger: globals.logger,
|
||||
terminal: globals.terminal,
|
||||
signals: globals.signals,
|
||||
processInfo: processInfo,
|
||||
reportReady: boolArg('report-ready'),
|
||||
pidFile: stringArg('pid-file'),
|
||||
)
|
||||
..setupTerminal()
|
||||
..registerSignalHandlers();
|
||||
..registerSignalHandlers()
|
||||
..setupTerminal();
|
||||
}));
|
||||
result = await runner.attach(
|
||||
appStartedCompleter: onAppStart,
|
||||
|
@ -289,11 +289,18 @@ class RunCommand extends RunCommandBase {
|
||||
help: 'Stay resident after launching the application. Not available with "--trace-startup".',
|
||||
)
|
||||
..addOption('pid-file',
|
||||
help: 'Specify a file to write the process id to. '
|
||||
help: 'Specify a file to write the process ID to. '
|
||||
'You can send SIGUSR1 to trigger a hot reload '
|
||||
'and SIGUSR2 to trigger a hot restart.',
|
||||
)
|
||||
..addFlag('benchmark',
|
||||
'and SIGUSR2 to trigger a hot restart. '
|
||||
'The file is created when the signal handlers '
|
||||
'are hooked and deleted when they are removed.',
|
||||
)..addFlag(
|
||||
'report-ready',
|
||||
help: 'Print "ready" to the console after handling a keyboard command.\n'
|
||||
'This is primarily useful for tests and other automation, but consider '
|
||||
'using --machine instead.',
|
||||
hide: !verboseHelp,
|
||||
)..addFlag('benchmark',
|
||||
negatable: false,
|
||||
hide: !verboseHelp,
|
||||
help: 'Enable a benchmarking mode. This will run the given application, '
|
||||
@ -514,8 +521,6 @@ class RunCommand extends RunCommandBase {
|
||||
final bool hotMode = shouldUseHotMode(buildInfo);
|
||||
final String applicationBinaryPath = stringArg('use-application-binary');
|
||||
|
||||
writePidFile(stringArg('pid-file'));
|
||||
|
||||
if (boolArg('machine')) {
|
||||
if (devices.length > 1) {
|
||||
throwToolExit('--machine does not support -d all.');
|
||||
@ -620,34 +625,39 @@ class RunCommand extends RunCommandBase {
|
||||
//
|
||||
// Do not add more operations to the future.
|
||||
final Completer<void> appStartedTimeRecorder = Completer<void>.sync();
|
||||
|
||||
TerminalHandler handler;
|
||||
// This callback can't throw.
|
||||
unawaited(appStartedTimeRecorder.future.then<void>(
|
||||
(_) {
|
||||
appStartedTime = globals.systemClock.now();
|
||||
if (stayResident) {
|
||||
TerminalHandler(
|
||||
handler = TerminalHandler(
|
||||
runner,
|
||||
logger: globals.logger,
|
||||
terminal: globals.terminal,
|
||||
signals: globals.signals,
|
||||
processInfo: processInfo,
|
||||
reportReady: boolArg('report-ready'),
|
||||
pidFile: stringArg('pid-file'),
|
||||
)
|
||||
..setupTerminal()
|
||||
..registerSignalHandlers();
|
||||
..registerSignalHandlers()
|
||||
..setupTerminal();
|
||||
}
|
||||
}
|
||||
));
|
||||
|
||||
try {
|
||||
final int result = await runner.run(
|
||||
appStartedCompleter: appStartedTimeRecorder,
|
||||
enableDevTools: stayResident && boolArg(FlutterCommand.kEnableDevTools),
|
||||
route: route,
|
||||
);
|
||||
handler?.stop();
|
||||
if (result != 0) {
|
||||
throwToolExit(null, exitCode: result);
|
||||
}
|
||||
} on RPCError catch (err) {
|
||||
if (err.code == RPCErrorCodes.kServiceDisappeared) {
|
||||
} on RPCError catch (error) {
|
||||
if (error.code == RPCErrorCodes.kServiceDisappeared) {
|
||||
throwToolExit('Lost connection to device.');
|
||||
}
|
||||
rethrow;
|
||||
|
@ -177,7 +177,9 @@ class DevtoolsServerLauncher extends DevtoolsLauncher {
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
devToolsUrl = null;
|
||||
if (devToolsUrl != null) {
|
||||
devToolsUrl = null;
|
||||
}
|
||||
if (_devToolsProcess != null) {
|
||||
_devToolsProcess.kill();
|
||||
await _devToolsProcess.exitCode;
|
||||
|
@ -969,7 +969,7 @@ abstract class ResidentRunner {
|
||||
await residentDevtoolsHandler.shutdown();
|
||||
await stopEchoingDeviceLog();
|
||||
await preExit();
|
||||
await exitApp();
|
||||
await exitApp(); // calls appFinished
|
||||
await shutdownDartDevelopmentService();
|
||||
}
|
||||
|
||||
@ -1452,17 +1452,27 @@ class TerminalHandler {
|
||||
@required Logger logger,
|
||||
@required Terminal terminal,
|
||||
@required Signals signals,
|
||||
@required io.ProcessInfo processInfo,
|
||||
@required bool reportReady,
|
||||
String pidFile,
|
||||
}) : _logger = logger,
|
||||
_terminal = terminal,
|
||||
_signals = signals;
|
||||
_signals = signals,
|
||||
_processInfo = processInfo,
|
||||
_reportReady = reportReady,
|
||||
_pidFile = pidFile;
|
||||
|
||||
final Logger _logger;
|
||||
final Terminal _terminal;
|
||||
final Signals _signals;
|
||||
final io.ProcessInfo _processInfo;
|
||||
final bool _reportReady;
|
||||
final String _pidFile;
|
||||
|
||||
final ResidentRunner residentRunner;
|
||||
bool _processingUserRequest = false;
|
||||
StreamSubscription<void> subscription;
|
||||
File _actualPidFile;
|
||||
|
||||
@visibleForTesting
|
||||
String lastReceivedCommand;
|
||||
@ -1476,7 +1486,6 @@ class TerminalHandler {
|
||||
subscription = _terminal.keystrokes.listen(processTerminalInput);
|
||||
}
|
||||
|
||||
|
||||
final Map<io.ProcessSignal, Object> _signalTokens = <io.ProcessSignal, Object>{};
|
||||
|
||||
void _addSignalHandler(io.ProcessSignal signal, SignalHandler handler) {
|
||||
@ -1485,19 +1494,30 @@ class TerminalHandler {
|
||||
|
||||
void registerSignalHandlers() {
|
||||
assert(residentRunner.stayResident);
|
||||
|
||||
_addSignalHandler(io.ProcessSignal.SIGINT, _cleanUp);
|
||||
_addSignalHandler(io.ProcessSignal.SIGTERM, _cleanUp);
|
||||
if (!residentRunner.supportsServiceProtocol || !residentRunner.supportsRestart) {
|
||||
return;
|
||||
if (residentRunner.supportsServiceProtocol && residentRunner.supportsRestart) {
|
||||
_addSignalHandler(io.ProcessSignal.SIGUSR1, _handleSignal);
|
||||
_addSignalHandler(io.ProcessSignal.SIGUSR2, _handleSignal);
|
||||
if (_pidFile != null) {
|
||||
_logger.printTrace('Writing pid to: $_pidFile');
|
||||
_actualPidFile = _processInfo.writePidFile(_pidFile);
|
||||
}
|
||||
}
|
||||
_addSignalHandler(io.ProcessSignal.SIGUSR1, _handleSignal);
|
||||
_addSignalHandler(io.ProcessSignal.SIGUSR2, _handleSignal);
|
||||
}
|
||||
|
||||
/// Unregisters terminal signal and keystroke handlers.
|
||||
void stop() {
|
||||
assert(residentRunner.stayResident);
|
||||
if (_actualPidFile != null) {
|
||||
try {
|
||||
_logger.printTrace('Deleting pid file (${_actualPidFile.path}).');
|
||||
_actualPidFile.deleteSync();
|
||||
} on FileSystemException catch (error) {
|
||||
_logger.printError('Failed to delete pid file (${_actualPidFile.path}): ${error.message}');
|
||||
}
|
||||
_actualPidFile = null;
|
||||
}
|
||||
for (final MapEntry<io.ProcessSignal, Object> entry in _signalTokens.entries) {
|
||||
_signals.removeHandler(entry.key, entry.value);
|
||||
}
|
||||
@ -1623,6 +1643,9 @@ class TerminalHandler {
|
||||
rethrow;
|
||||
} finally {
|
||||
_processingUserRequest = false;
|
||||
if (_reportReady) {
|
||||
_logger.printStatus('ready');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -6,6 +6,8 @@
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:file/memory.dart';
|
||||
import 'package:flutter_tools/src/base/io.dart';
|
||||
import 'package:flutter_tools/src/base/logger.dart';
|
||||
import 'package:flutter_tools/src/base/signals.dart';
|
||||
import 'package:flutter_tools/src/base/terminal.dart';
|
||||
@ -22,11 +24,15 @@ void main() {
|
||||
final Logger logger = BufferLogger.test();
|
||||
final Signals signals = Signals.test();
|
||||
final Terminal terminal = Terminal.test();
|
||||
final MemoryFileSystem fs = MemoryFileSystem.test();
|
||||
final ProcessInfo processInfo = ProcessInfo.test(fs);
|
||||
final TerminalHandler terminalHandler = TerminalHandler(
|
||||
testRunner,
|
||||
logger: logger,
|
||||
signals: signals,
|
||||
terminal: terminal,
|
||||
processInfo: processInfo,
|
||||
reportReady: false,
|
||||
);
|
||||
|
||||
expect(testRunner.hasHelpBeenPrinted, false);
|
||||
@ -39,11 +45,15 @@ void main() {
|
||||
final Logger logger = BufferLogger.test();
|
||||
final Signals signals = Signals.test();
|
||||
final Terminal terminal = Terminal.test();
|
||||
final MemoryFileSystem fs = MemoryFileSystem.test();
|
||||
final ProcessInfo processInfo = ProcessInfo.test(fs);
|
||||
final TerminalHandler terminalHandler = TerminalHandler(
|
||||
testRunner,
|
||||
logger: logger,
|
||||
signals: signals,
|
||||
terminal: terminal,
|
||||
processInfo: processInfo,
|
||||
reportReady: false,
|
||||
);
|
||||
|
||||
expect(testRunner.hasHelpBeenPrinted, false);
|
||||
@ -60,12 +70,16 @@ void main() {
|
||||
testLogger = BufferLogger.test();
|
||||
final Signals signals = Signals.test();
|
||||
final Terminal terminal = Terminal.test();
|
||||
final MemoryFileSystem fs = MemoryFileSystem.test();
|
||||
final ProcessInfo processInfo = ProcessInfo.test(fs);
|
||||
mockResidentRunner = MockResidentRunner();
|
||||
terminalHandler = TerminalHandler(
|
||||
mockResidentRunner,
|
||||
logger: testLogger,
|
||||
signals: signals,
|
||||
terminal: terminal,
|
||||
processInfo: processInfo,
|
||||
reportReady: false,
|
||||
);
|
||||
when(mockResidentRunner.supportsServiceProtocol).thenReturn(true);
|
||||
});
|
||||
@ -310,6 +324,34 @@ void main() {
|
||||
verify(mockResidentRunner.debugToggleDebugCheckElevationsEnabled()).called(2);
|
||||
});
|
||||
});
|
||||
|
||||
testWithoutContext('pidfile creation', () {
|
||||
final BufferLogger testLogger = BufferLogger.test();
|
||||
final Signals signals = _TestSignals(Signals.defaultExitSignals);
|
||||
final Terminal terminal = Terminal.test();
|
||||
final MemoryFileSystem fs = MemoryFileSystem.test();
|
||||
final ProcessInfo processInfo = ProcessInfo.test(fs);
|
||||
final ResidentRunner mockResidentRunner = MockResidentRunner();
|
||||
when(mockResidentRunner.stayResident).thenReturn(true);
|
||||
when(mockResidentRunner.supportsServiceProtocol).thenReturn(true);
|
||||
when(mockResidentRunner.supportsRestart).thenReturn(true);
|
||||
const String filename = 'test.pid';
|
||||
final TerminalHandler terminalHandler = TerminalHandler(
|
||||
mockResidentRunner,
|
||||
logger: testLogger,
|
||||
signals: signals,
|
||||
terminal: terminal,
|
||||
processInfo: processInfo,
|
||||
reportReady: false,
|
||||
pidFile: filename,
|
||||
);
|
||||
expect(fs.file(filename).existsSync(), isFalse);
|
||||
terminalHandler.setupTerminal();
|
||||
terminalHandler.registerSignalHandlers();
|
||||
expect(fs.file(filename).existsSync(), isTrue);
|
||||
terminalHandler.stop();
|
||||
expect(fs.file(filename).existsSync(), isFalse);
|
||||
});
|
||||
}
|
||||
|
||||
class MockDevice extends Mock implements Device {
|
||||
@ -353,3 +395,35 @@ class TestRunner extends Mock implements ResidentRunner {
|
||||
bool enableDevTools = false,
|
||||
}) async => null;
|
||||
}
|
||||
|
||||
class _TestSignals implements Signals {
|
||||
_TestSignals(this.exitSignals);
|
||||
|
||||
final List<ProcessSignal> exitSignals;
|
||||
|
||||
final Map<ProcessSignal, Map<Object, SignalHandler>> _handlersTable =
|
||||
<ProcessSignal, Map<Object, SignalHandler>>{};
|
||||
|
||||
@override
|
||||
Object addHandler(ProcessSignal signal, SignalHandler handler) {
|
||||
final Object token = Object();
|
||||
_handlersTable.putIfAbsent(signal, () => <Object, SignalHandler>{})[token] = handler;
|
||||
return token;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> removeHandler(ProcessSignal signal, Object token) async {
|
||||
if (!_handlersTable.containsKey(signal)) {
|
||||
return false;
|
||||
}
|
||||
if (!_handlersTable[signal].containsKey(token)) {
|
||||
return false;
|
||||
}
|
||||
_handlersTable[signal].remove(token);
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<Object> get errors => _errors.stream;
|
||||
final StreamController<Object> _errors = StreamController<Object>();
|
||||
}
|
||||
|
@ -39,16 +39,6 @@ void main() {
|
||||
tryToDelete(tempDir);
|
||||
});
|
||||
|
||||
testWithoutContext('writes pid-file', () async {
|
||||
final File pidFile = tempDir.childFile('test.pid');
|
||||
await _flutterRun.run(withDebugger: true);
|
||||
await _flutterAttach.attach(
|
||||
_flutterRun.vmServicePort,
|
||||
pidFile: pidFile,
|
||||
);
|
||||
expect(pidFile.existsSync(), isTrue);
|
||||
});
|
||||
|
||||
testWithoutContext('can hot reload', () async {
|
||||
await _flutterRun.run(withDebugger: true);
|
||||
await _flutterAttach.attach(_flutterRun.vmServicePort);
|
||||
|
@ -51,12 +51,6 @@ void main() {
|
||||
}
|
||||
});
|
||||
|
||||
testWithoutContext('flutter run writes pid-file', () async {
|
||||
final File pidFile = tempDir.childFile('test.pid');
|
||||
await _flutter.run(pidFile: pidFile);
|
||||
expect(pidFile.existsSync(), isTrue);
|
||||
});
|
||||
|
||||
testWithoutContext('sets activeDevToolsServerAddress extension', () async {
|
||||
await _flutter.run(
|
||||
startPaused: true,
|
||||
|
@ -103,36 +103,4 @@ void main() {
|
||||
|
||||
expect(stdout.toString(), isNot(contains(_exceptionStart)));
|
||||
});
|
||||
|
||||
testWithoutContext('flutter run for web reports an early error in an application', () async {
|
||||
final StringBuffer stdout = StringBuffer();
|
||||
|
||||
await _flutter.run(
|
||||
startPaused: true,
|
||||
withDebugger: true,
|
||||
structuredErrors: true,
|
||||
chrome: true,
|
||||
machine: false,
|
||||
);
|
||||
await _flutter.resume();
|
||||
final Completer<void> completer = Completer<void>();
|
||||
bool lineFound = false;
|
||||
|
||||
await Future<void>(() async {
|
||||
_flutter.stdout.listen((String line) {
|
||||
stdout.writeln(line);
|
||||
if (line.startsWith('Another exception was thrown') && !lineFound) {
|
||||
lineFound = true;
|
||||
completer.complete();
|
||||
}
|
||||
});
|
||||
await completer.future;
|
||||
}).timeout(const Duration(seconds: 15), onTimeout: () {
|
||||
// Complete anyway in case we don't see the 'Another exception' line.
|
||||
completer.complete();
|
||||
});
|
||||
|
||||
expect(stdout.toString(), contains(_exceptionStart));
|
||||
await _flutter.stop();
|
||||
}, skip: 'Running in cirrus environment causes premature exit');
|
||||
}
|
||||
|
@ -0,0 +1,587 @@
|
||||
// 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.
|
||||
|
||||
// The purpose of this test is to verify the end-to-end behavior of
|
||||
// "flutter run" and other such commands, as closely as possible to
|
||||
// the default behavior. To that end, it avoids the use of any test
|
||||
// features that are not critical (-dflutter-test being the primary
|
||||
// example of a test feature that it does use). For example, no use
|
||||
// is made of "--machine" in these tests.
|
||||
|
||||
// There are a number of risks when it comes to writing a test such
|
||||
// as this one. Typically these tests are hard to debug if they are
|
||||
// in a failing condition, because they just hang as they await the
|
||||
// next expected line that never comes. To avoid this, here we have
|
||||
// the policy of looking for multiple lines, printing what expected
|
||||
// lines were not seen when a short timeout expires (but timing out
|
||||
// does not cause the test to fail, to reduce flakes), and wherever
|
||||
// possible recording all output and comparing the actual output to
|
||||
// the expected output only once the test is completed.
|
||||
|
||||
// To aid in debugging, consider passing the `debug: true` argument
|
||||
// to the runFlutter function.
|
||||
|
||||
// @dart = 2.8
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:pedantic/pedantic.dart';
|
||||
import 'package:process/process.dart';
|
||||
|
||||
import '../src/common.dart';
|
||||
import 'test_utils.dart' show fileSystem;
|
||||
|
||||
const ProcessManager processManager = LocalProcessManager();
|
||||
final String flutterRoot = getFlutterRoot();
|
||||
final String flutterBin = fileSystem.path.join(flutterRoot, 'bin', 'flutter');
|
||||
|
||||
typedef LineHandler = String/*?*/ Function(String line);
|
||||
|
||||
abstract class Transition {
|
||||
const Transition({this.handler, this.logging});
|
||||
|
||||
/// Callback that is invoked when the transition matches.
|
||||
///
|
||||
/// This should not throw, even if the test is failing. (For example, don't use "expect"
|
||||
/// in these callbacks.) Throwing here would prevent the [runFlutter] function from running
|
||||
/// to completion, which would leave zombie `flutter` processes around.
|
||||
final LineHandler/*?*/ handler;
|
||||
|
||||
/// Whether to enable or disable logging when this transition is matched.
|
||||
///
|
||||
/// The default value, null, leaves the logging state unaffected.
|
||||
final bool/*?*/ logging;
|
||||
|
||||
bool matches(String line);
|
||||
|
||||
@protected
|
||||
bool lineMatchesPattern(String line, Pattern pattern) {
|
||||
if (pattern is String) {
|
||||
return line == pattern;
|
||||
}
|
||||
return line.contains(pattern);
|
||||
}
|
||||
|
||||
@protected
|
||||
String describe(Pattern pattern) {
|
||||
if (pattern is String) {
|
||||
return '"$pattern"';
|
||||
}
|
||||
if (pattern is RegExp) {
|
||||
return '/${pattern.pattern}/';
|
||||
}
|
||||
return '$pattern';
|
||||
}
|
||||
}
|
||||
|
||||
class Barrier extends Transition {
|
||||
const Barrier(this.pattern, {LineHandler/*?*/ handler, bool/*?*/ logging}) : super(handler: handler, logging: logging);
|
||||
final Pattern pattern;
|
||||
|
||||
@override
|
||||
bool matches(String line) => lineMatchesPattern(line, pattern);
|
||||
|
||||
@override
|
||||
String toString() => describe(pattern);
|
||||
}
|
||||
|
||||
class Multiple extends Transition {
|
||||
Multiple(List<Pattern> patterns, {
|
||||
LineHandler/*?*/ handler,
|
||||
bool/*?*/ logging,
|
||||
}) : _originalPatterns = patterns,
|
||||
patterns = patterns.toList(),
|
||||
super(handler: handler, logging: logging);
|
||||
|
||||
final List<Pattern> _originalPatterns;
|
||||
final List<Pattern> patterns;
|
||||
|
||||
@override
|
||||
bool matches(String line) {
|
||||
for (int index = 0; index < patterns.length; index += 1) {
|
||||
if (lineMatchesPattern(line, patterns[index])) {
|
||||
patterns.removeAt(index);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return patterns.isEmpty;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return _originalPatterns.map(describe).join(', ') + ' (matched ${_originalPatterns.length - patterns.length} so far)';
|
||||
}
|
||||
}
|
||||
|
||||
class ProcessTestResult {
|
||||
const ProcessTestResult(this.exitCode, this.stdout, this.stderr);
|
||||
final int exitCode;
|
||||
final List<String> stdout;
|
||||
final List<String> stderr;
|
||||
|
||||
@override
|
||||
String toString() => 'exit code $exitCode\nstdout:\n ${stdout.join('\n ')}\nstderr:\n ${stderr.join('\n ')}\n';
|
||||
}
|
||||
|
||||
String clarify(String line) {
|
||||
return line.runes.map<String>((int rune) {
|
||||
if (rune >= 0x20 && rune <= 0x7F) {
|
||||
return String.fromCharCode(rune);
|
||||
}
|
||||
switch (rune) {
|
||||
case 0x00: return '<NUL>';
|
||||
case 0x07: return '<BEL>';
|
||||
case 0x08: return '<TAB>';
|
||||
case 0x09: return '<BS>';
|
||||
case 0x0A: return '<LF>';
|
||||
case 0x0D: return '<CR>';
|
||||
}
|
||||
return '<${rune.toRadixString(16).padLeft(rune <= 0xFF ? 2 : rune <= 0xFFFF ? 4 : 5, '0')}>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
void printClearly(String line) {
|
||||
print(clarify(line));
|
||||
}
|
||||
|
||||
Future<ProcessTestResult> runFlutter(
|
||||
List<String> arguments,
|
||||
String workingDirectory,
|
||||
List<Transition> transitions, {
|
||||
bool debug = false,
|
||||
bool logging = true,
|
||||
Duration expectedMaxDuration = const Duration(seconds: 25), // must be less than test timeout of 30 seconds!
|
||||
}) async {
|
||||
final Process process = await processManager.start(
|
||||
<String>[flutterBin, ...arguments],
|
||||
workingDirectory: workingDirectory,
|
||||
);
|
||||
final List<String> stdoutLog = <String>[];
|
||||
final List<String> stderrLog = <String>[];
|
||||
final List<String> stdinLog = <String>[];
|
||||
int nextTransition = 0;
|
||||
void describeStatus() {
|
||||
if (transitions.isNotEmpty) {
|
||||
print('Expected state transitions:');
|
||||
for (int index = 0; index < transitions.length; index += 1) {
|
||||
print(
|
||||
'${index.toString().padLeft(5)} '
|
||||
'${index < nextTransition ? 'ALREADY MATCHED ' :
|
||||
index == nextTransition ? 'NOW WAITING FOR>' :
|
||||
' '} ${transitions[index]}');
|
||||
}
|
||||
}
|
||||
if (stdoutLog.isEmpty && stderrLog.isEmpty && stdinLog.isEmpty) {
|
||||
print('So far nothing has been logged${ debug ? "" : "; use debug:true to print all output" }.');
|
||||
} else {
|
||||
print('Log${ debug ? "" : " (only contains logged lines; use debug:true to print all output)" }:');
|
||||
stdoutLog.map<String>((String line) => 'stdout: $line').forEach(printClearly);
|
||||
stderrLog.map<String>((String line) => 'stderr: $line').forEach(printClearly);
|
||||
stdinLog.map<String>((String line) => 'stdin: $line').forEach(printClearly);
|
||||
}
|
||||
}
|
||||
bool streamingLogs = false;
|
||||
Timer/*?*/ timeout;
|
||||
void processTimeout() {
|
||||
if (!streamingLogs) {
|
||||
streamingLogs = true;
|
||||
if (!debug) {
|
||||
print('Test is taking a long time.');
|
||||
}
|
||||
describeStatus();
|
||||
print('(streaming all logs from this point on...)');
|
||||
} else {
|
||||
print('(taking a long time...)');
|
||||
}
|
||||
}
|
||||
void processStdout(String line) {
|
||||
if (logging) {
|
||||
stdoutLog.add(line);
|
||||
}
|
||||
if (streamingLogs) {
|
||||
print('stdout: $line');
|
||||
}
|
||||
if (nextTransition < transitions.length && transitions[nextTransition].matches(line)) {
|
||||
if (streamingLogs) {
|
||||
print('(matched ${transitions[nextTransition]})');
|
||||
}
|
||||
if (transitions[nextTransition].logging != null) {
|
||||
if (!logging && transitions[nextTransition].logging/*!*/) {
|
||||
stdoutLog.add(line);
|
||||
}
|
||||
logging = transitions[nextTransition].logging/*!*/;
|
||||
if (streamingLogs) {
|
||||
if (logging) {
|
||||
print('(enabled logging)');
|
||||
} else {
|
||||
print('(disabled logging)');
|
||||
}
|
||||
}
|
||||
}
|
||||
if (transitions[nextTransition].handler != null) {
|
||||
final String/*?*/ command = transitions[nextTransition].handler/*!*/(line);
|
||||
if (command != null) {
|
||||
stdinLog.add(command);
|
||||
if (streamingLogs) {
|
||||
print('stdin: $command');
|
||||
}
|
||||
process.stdin.write(command);
|
||||
}
|
||||
}
|
||||
nextTransition += 1;
|
||||
timeout?.cancel();
|
||||
timeout = Timer(expectedMaxDuration ~/ 5, processTimeout);
|
||||
}
|
||||
}
|
||||
void processStderr(String line) {
|
||||
stderrLog.add(line);
|
||||
if (streamingLogs) {
|
||||
print('stderr: $line');
|
||||
}
|
||||
}
|
||||
if (debug) {
|
||||
processTimeout();
|
||||
} else {
|
||||
timeout = Timer(expectedMaxDuration ~/ 2, processTimeout);
|
||||
}
|
||||
process.stdout.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).listen(processStdout);
|
||||
process.stderr.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).listen(processStderr);
|
||||
unawaited(process.exitCode.timeout(expectedMaxDuration, onTimeout: () {
|
||||
print('(process is not quitting, trying to send a "q" just in case that helps)');
|
||||
print('(a functional test should never reach this point)');
|
||||
process.stdin.write('q');
|
||||
return null;
|
||||
}).catchError((Object error) { /* ignore the error here, it'll be reported on the next line */ }));
|
||||
final int exitCode = await process.exitCode;
|
||||
if (streamingLogs) {
|
||||
print('(process terminated with exit code $exitCode)');
|
||||
}
|
||||
timeout?.cancel();
|
||||
if (nextTransition < transitions.length) {
|
||||
print('The subprocess terminated before all the expected transitions had been matched.');
|
||||
if (stderrLog.any((String line) => line.contains('Oops; flutter has exited unexpectedly:'))) {
|
||||
print('The subprocess may in fact have crashed. Check the stderr logs below.');
|
||||
}
|
||||
print('The transition that we were hoping to see next but that we never saw was:');
|
||||
print('${nextTransition.toString().padLeft(5)} NOW WAITING FOR> ${transitions[nextTransition]}');
|
||||
if (!streamingLogs) {
|
||||
describeStatus();
|
||||
print('(process terminated with exit code $exitCode)');
|
||||
}
|
||||
throw TestFailure('Missed some expected transitions.');
|
||||
}
|
||||
if (streamingLogs) {
|
||||
print('(completed execution successfully!)');
|
||||
}
|
||||
return ProcessTestResult(exitCode, stdoutLog, stderrLog);
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWithoutContext('flutter run writes and clears pidfile appropriately', () async {
|
||||
final String tempDirectory = fileSystem.systemTempDirectory.createTempSync('flutter_overall_experience_test.').resolveSymbolicLinksSync();
|
||||
final String pidFile = fileSystem.path.join(tempDirectory, 'flutter.pid');
|
||||
final String testDirectory = fileSystem.path.join(flutterRoot, 'examples', 'hello_world');
|
||||
bool/*?*/ existsDuringTest;
|
||||
try {
|
||||
expect(fileSystem.file(pidFile).existsSync(), isFalse);
|
||||
final ProcessTestResult result = await runFlutter(
|
||||
<String>['run', '-dflutter-tester', '--pid-file', pidFile],
|
||||
testDirectory,
|
||||
<Transition>[
|
||||
Barrier('q Quit (terminate the application on the device).', handler: (String line) {
|
||||
existsDuringTest = fileSystem.file(pidFile).existsSync();
|
||||
return 'q';
|
||||
}),
|
||||
const Barrier('Application finished.'),
|
||||
],
|
||||
);
|
||||
expect(existsDuringTest, isNot(isNull));
|
||||
expect(existsDuringTest, isTrue);
|
||||
expect(result.exitCode, 0, reason: 'subprocess failed; $result');
|
||||
expect(fileSystem.file(pidFile).existsSync(), isFalse);
|
||||
// This first test ignores the stdout and stderr, so that if the
|
||||
// first run outputs "building flutter", or the "there's a new
|
||||
// flutter" banner, or other such first-run messages, they won't
|
||||
// fail the tests. This does mean that running this test first is
|
||||
// actually important in the case where you're running the tests
|
||||
// manually. (On CI, all those messages are expected to be seen
|
||||
// long before we get here, e.g. because we run "flutter doctor".)
|
||||
} finally {
|
||||
tryToDelete(fileSystem.directory(tempDirectory));
|
||||
}
|
||||
});
|
||||
|
||||
testWithoutContext('flutter run handle SIGUSR1/2', () async {
|
||||
final String tempDirectory = fileSystem.systemTempDirectory.createTempSync('flutter_overall_experience_test.').resolveSymbolicLinksSync();
|
||||
final String pidFile = fileSystem.path.join(tempDirectory, 'flutter.pid');
|
||||
final String testDirectory = fileSystem.path.join(flutterRoot, 'dev', 'integration_tests', 'ui');
|
||||
final String testScript = fileSystem.path.join('lib', 'commands.dart');
|
||||
/*late*/ int pid;
|
||||
try {
|
||||
final ProcessTestResult result = await runFlutter(
|
||||
<String>['run', '-dflutter-tester', '--report-ready', '--pid-file', pidFile, '--no-devtools', testScript],
|
||||
testDirectory,
|
||||
<Transition>[
|
||||
Barrier('Flutter run key commands.', handler: (String line) {
|
||||
pid = int.parse(fileSystem.file(pidFile).readAsStringSync());
|
||||
processManager.killPid(pid, ProcessSignal.sigusr1);
|
||||
return null;
|
||||
}),
|
||||
Barrier(RegExp(r'^Performing hot reload\.\.\.'), logging: true),
|
||||
Multiple(<Pattern>[RegExp(r'^Reloaded 0 libraries in [0-9]+ms\.$'), /*'called reassemble', (see TODO below)*/ 'called paint'], handler: (String line) {
|
||||
processManager.killPid(pid, ProcessSignal.sigusr2);
|
||||
return null;
|
||||
}),
|
||||
Barrier(RegExp(r'^Performing hot restart\.\.\.')),
|
||||
Multiple(<Pattern>[RegExp(r'^Restarted application in [0-9]+ms.$'), 'called main', 'called paint'], handler: (String line) {
|
||||
return 'q';
|
||||
}),
|
||||
const Barrier('Application finished.'),
|
||||
],
|
||||
logging: false, // we ignore leading log lines to avoid making this test sensitive to e.g. the help message text
|
||||
);
|
||||
// We check the output from the app (all starts with "called ...") and the output from the tool
|
||||
// (everything else) separately, because their relative timing isn't guaranteed. Their rough timing
|
||||
// is verified by the expected transitions above.
|
||||
// TODO(ianh): Fix the tool so that the output isn't garbled (right now we're putting debug output from
|
||||
// the app on the line where we're spinning the busy signal, rather than adding a newline).
|
||||
expect(result.stdout.where((String line) => line.startsWith('called ') && line != 'called reassemble' /* see todo above*/), <Object>[
|
||||
// logs start after we receive the response to sending SIGUSR1
|
||||
// SIGUSR1:
|
||||
// 'called reassemble', // see todo above, this only sometimes gets included, other times it's on the "performing..." line
|
||||
'called paint',
|
||||
// SIGUSR2:
|
||||
'called main',
|
||||
'called paint',
|
||||
]);
|
||||
expect(result.stdout.where((String line) => !line.startsWith('called ')), <Object>[
|
||||
// logs start after we receive the response to sending SIGUSR1
|
||||
startsWith('Performing hot reload...'), // see todo above, this sometimes ends with "called reassemble"
|
||||
'', // this newline is probably the misplaced one for the reassemble; see todo above
|
||||
startsWith('Reloaded 0 libraries in '),
|
||||
'Performing hot restart... ',
|
||||
startsWith('Restarted application in '),
|
||||
'', // this newline is the one for after we hit "q"
|
||||
'Application finished.',
|
||||
'ready',
|
||||
]);
|
||||
expect(result.exitCode, 0);
|
||||
} finally {
|
||||
tryToDelete(fileSystem.directory(tempDirectory));
|
||||
}
|
||||
}, skip: Platform.isWindows); // Windows doesn't support sending signals.
|
||||
|
||||
testWithoutContext('flutter run can hot reload and hot restart, handle "p" key', () async {
|
||||
final String tempDirectory = fileSystem.systemTempDirectory.createTempSync('flutter_overall_experience_test.').resolveSymbolicLinksSync();
|
||||
final String testDirectory = fileSystem.path.join(flutterRoot, 'dev', 'integration_tests', 'ui');
|
||||
final String testScript = fileSystem.path.join('lib', 'commands.dart');
|
||||
try {
|
||||
final ProcessTestResult result = await runFlutter(
|
||||
<String>['run', '-dflutter-tester', '--report-ready', '--no-devtools', testScript],
|
||||
testDirectory,
|
||||
<Transition>[
|
||||
Multiple(<Pattern>['Flutter run key commands.', 'called main'], handler: (String line) {
|
||||
return 'r';
|
||||
}),
|
||||
Barrier(RegExp(r'^Performing hot reload\.\.\.'), logging: true),
|
||||
Multiple(<Pattern>['ready', /*'reassemble', (see todo below)*/ 'called paint'], handler: (String line) {
|
||||
return 'R';
|
||||
}),
|
||||
Barrier(RegExp(r'^Performing hot restart\.\.\.')),
|
||||
Multiple(<Pattern>['ready', 'called main', 'called paint'], handler: (String line) {
|
||||
return 'p';
|
||||
}),
|
||||
Multiple(<Pattern>['ready', 'called paint', 'called debugPaintSize'], handler: (String line) {
|
||||
return 'p';
|
||||
}),
|
||||
Multiple(<Pattern>['ready', 'called paint'], handler: (String line) {
|
||||
return 'q';
|
||||
}),
|
||||
const Barrier('Application finished.'),
|
||||
],
|
||||
logging: false, // we ignore leading log lines to avoid making this test sensitive to e.g. the help message text
|
||||
);
|
||||
// We check the output from the app (all starts with "called ...") and the output from the tool
|
||||
// (everything else) separately, because their relative timing isn't guaranteed. Their rough timing
|
||||
// is verified by the expected transitions above.
|
||||
// TODO(ianh): Fix the tool so that the output isn't garbled (right now we're putting debug output from
|
||||
// the app on the line where we're spinning the busy signal, rather than adding a newline).
|
||||
expect(result.stdout.where((String line) => line.startsWith('called ') && line != 'called reassemble' /* see todo above*/), <Object>[
|
||||
// hot reload:
|
||||
// 'called reassemble', // see todo above, this sometimes gets placed on the "Performing hot reload..." line
|
||||
'called paint',
|
||||
// hot restart:
|
||||
'called main',
|
||||
'called paint',
|
||||
// debugPaintSizeEnabled = true:
|
||||
'called paint',
|
||||
'called debugPaintSize',
|
||||
// debugPaintSizeEnabled = false:
|
||||
'called paint',
|
||||
]);
|
||||
expect(result.stdout.where((String line) => !line.startsWith('called ')), <Object>[
|
||||
// logs start after we receive the response to hitting "r"
|
||||
startsWith('Performing hot reload...'), // see todo above, this sometimes ends with "called reassemble"
|
||||
'', // this newline is probably the misplaced one for the reassemble; see todo above
|
||||
startsWith('Reloaded 0 libraries in '),
|
||||
'ready',
|
||||
'', // this newline is the one for after we hit "R"
|
||||
'Performing hot restart... ',
|
||||
startsWith('Restarted application in '),
|
||||
'ready',
|
||||
'', // newline for after we hit "p" the first time
|
||||
'ready',
|
||||
'', // newline for after we hit "p" the second time
|
||||
'ready',
|
||||
'', // this newline is the one for after we hit "q"
|
||||
'Application finished.',
|
||||
'ready',
|
||||
]);
|
||||
expect(result.exitCode, 0);
|
||||
} finally {
|
||||
tryToDelete(fileSystem.directory(tempDirectory));
|
||||
}
|
||||
});
|
||||
|
||||
testWithoutContext('flutter error messages include a DevTools link', () async {
|
||||
final String tempDirectory = fileSystem.systemTempDirectory.createTempSync('flutter_overall_experience_test.').resolveSymbolicLinksSync();
|
||||
final String testDirectory = fileSystem.path.join(flutterRoot, 'dev', 'integration_tests', 'ui');
|
||||
final String testScript = fileSystem.path.join('lib', 'overflow.dart');
|
||||
try {
|
||||
final ProcessTestResult result = await runFlutter(
|
||||
<String>['run', '-dflutter-tester', testScript],
|
||||
testDirectory,
|
||||
<Transition>[
|
||||
Barrier(RegExp(r'^An Observatory debugger and profiler on Flutter test device is available at: ')),
|
||||
Barrier(RegExp(r'^The Flutter DevTools debugger and profiler on Flutter test device is available at: '), handler: (String line) {
|
||||
return 'r';
|
||||
}),
|
||||
Barrier(RegExp(r'^Performing hot reload\.\.\.'), logging: true),
|
||||
Barrier(RegExp(r'^Reloaded 0 libraries in [0-9]+ms.'), handler: (String line) {
|
||||
return 'q';
|
||||
}),
|
||||
],
|
||||
logging: false,
|
||||
);
|
||||
expect(result.exitCode, 0);
|
||||
expect(result.stdout, <Object>[
|
||||
startsWith('Performing hot reload...'),
|
||||
'══╡ EXCEPTION CAUGHT BY RENDERING LIBRARY ╞═════════════════════════════════════════════════════════',
|
||||
'The following assertion was thrown during layout:',
|
||||
'A RenderFlex overflowed by 69200 pixels on the right.',
|
||||
'',
|
||||
'The relevant error-causing widget was:',
|
||||
matches(RegExp(r'^ Row .+flutter/dev/integration_tests/ui/lib/overflow\.dart:31:12$')),
|
||||
'',
|
||||
'To inspect this widget in Flutter DevTools, visit:',
|
||||
startsWith('http'),
|
||||
'',
|
||||
'The overflowing RenderFlex has an orientation of Axis.horizontal.',
|
||||
'The edge of the RenderFlex that is overflowing has been marked in the rendering with a yellow and',
|
||||
'black striped pattern. This is usually caused by the contents being too big for the RenderFlex.',
|
||||
'Consider applying a flex factor (e.g. using an Expanded widget) to force the children of the',
|
||||
'RenderFlex to fit within the available space instead of being sized to their natural size.',
|
||||
'This is considered an error condition because it indicates that there is content that cannot be',
|
||||
'seen. If the content is legitimately bigger than the available space, consider clipping it with a',
|
||||
'ClipRect widget before putting it in the flex, or using a scrollable container rather than a Flex,',
|
||||
'like a ListView.',
|
||||
matches(RegExp(r'^The specific RenderFlex in question is: RenderFlex#..... OVERFLOWING:$')),
|
||||
startsWith(' creator: Row ← Test ← '),
|
||||
contains(' ← '),
|
||||
endsWith(' ← ⋯'),
|
||||
' parentData: <none> (can use size)',
|
||||
' constraints: BoxConstraints(w=800.0, h=600.0)',
|
||||
' size: Size(800.0, 600.0)',
|
||||
' direction: horizontal',
|
||||
' mainAxisAlignment: start',
|
||||
' mainAxisSize: max',
|
||||
' crossAxisAlignment: center',
|
||||
' textDirection: ltr',
|
||||
' verticalDirection: down',
|
||||
'◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤',
|
||||
'════════════════════════════════════════════════════════════════════════════════════════════════════',
|
||||
'',
|
||||
startsWith('Reloaded 0 libraries in '),
|
||||
'',
|
||||
'Application finished.',
|
||||
]);
|
||||
} finally {
|
||||
tryToDelete(fileSystem.directory(tempDirectory));
|
||||
}
|
||||
}, skip: 'DevTools does not reliably launch on bots currently.'); // TODO(ianh): fix and re-enable test.
|
||||
|
||||
testWithoutContext('flutter run help output', () async {
|
||||
// This test enables all logging so that it checks the exact text of starting up an application.
|
||||
// The idea is to verify that we're not outputting spurious messages.
|
||||
// WHEN EDITING THIS TEST PLEASE CAREFULLY CONSIDER WHETHER THE NEW OUTPUT IS AN IMPROVEMENT.
|
||||
final String testDirectory = fileSystem.path.join(flutterRoot, 'examples', 'hello_world');
|
||||
final RegExp finalLine = RegExp(r'^An Observatory'); /* RegExp(r'^The Flutter DevTools'); */ // TODO(ianh): use this when enabling devtools
|
||||
final ProcessTestResult result = await runFlutter(
|
||||
<String>['run', '-dflutter-tester', '--no-devtools'], // TODO(ianh): enable devtools
|
||||
testDirectory,
|
||||
<Transition>[
|
||||
Barrier(finalLine, handler: (String line) {
|
||||
return 'h';
|
||||
}),
|
||||
Barrier(finalLine, handler: (String line) {
|
||||
return 'q';
|
||||
}),
|
||||
const Barrier('Application finished.'),
|
||||
],
|
||||
);
|
||||
expect(result.exitCode, 0);
|
||||
expect(result.stderr, isEmpty);
|
||||
expect(result.stdout, <Object>[
|
||||
startsWith('Launching '),
|
||||
startsWith('Syncing files to device Flutter test device...'),
|
||||
'',
|
||||
'Flutter run key commands.',
|
||||
startsWith('r Hot reload.'),
|
||||
'R Hot restart.',
|
||||
'h Repeat this help message.',
|
||||
'd Detach (terminate "flutter run" but leave application running).',
|
||||
'c Clear the screen',
|
||||
'q Quit (terminate the application on the device).',
|
||||
'',
|
||||
contains('Running with sound null safety'),
|
||||
'',
|
||||
startsWith('An Observatory debugger and profiler on Flutter test device is available at: http://'),
|
||||
/* startsWith('The Flutter DevTools debugger and profiler on Flutter test device is available at: http://'), */ // TODO(ianh): enable devtools
|
||||
'',
|
||||
'Flutter run key commands.',
|
||||
startsWith('r Hot reload.'),
|
||||
'R Hot restart.',
|
||||
'h Repeat this help message.',
|
||||
'd Detach (terminate "flutter run" but leave application running).',
|
||||
'c Clear the screen',
|
||||
'q Quit (terminate the application on the device).',
|
||||
'b Toggle the platform brightness setting (dark and light mode). (debugBrightnessOverride)',
|
||||
'w Dump widget hierarchy to the console. (debugDumpApp)',
|
||||
't Dump rendering tree to the console. (debugDumpRenderTree)',
|
||||
'L Dump layer tree to the console. (debugDumpLayerTree)',
|
||||
'S Dump accessibility tree in traversal order. (debugDumpSemantics)',
|
||||
'U Dump accessibility tree in inverse hit test order. (debugDumpSemantics)',
|
||||
'i Toggle widget inspector. (WidgetsApp.showWidgetInspectorOverride)',
|
||||
startsWith('I Toggle oversized image inversion'),
|
||||
'p Toggle the display of construction lines. (debugPaintSizeEnabled)',
|
||||
'o Simulate different operating systems. (defaultTargetPlatform)',
|
||||
'z Toggle elevation checker.',
|
||||
'g Run source code generators.',
|
||||
'M Write SkSL shaders to a unique file in the project directory.',
|
||||
'v Launch DevTools.', // TODO(ianh): hide this when devtools isn't available
|
||||
'P Toggle performance overlay. (WidgetsApp.showPerformanceOverlay)',
|
||||
'a Toggle timeline events for all widget build methods. (debugProfileWidgetBuilds)',
|
||||
'',
|
||||
contains('Running with sound null safety'),
|
||||
'',
|
||||
startsWith('An Observatory debugger and profiler on Flutter test device is available at: http://'),
|
||||
/* startsWith('The Flutter DevTools debugger and profiler on Flutter test device is available at: http://'), */ // TODO(ianh): enable devtools
|
||||
'',
|
||||
'Application finished.',
|
||||
]);
|
||||
});
|
||||
}
|
@ -86,7 +86,6 @@ abstract class FlutterTestDriver {
|
||||
List<String> arguments, {
|
||||
String script,
|
||||
bool withDebugger = false,
|
||||
File pidFile,
|
||||
bool singleWidgetReloads = false,
|
||||
}) async {
|
||||
final String flutterBin = fileSystem.path.join(getFlutterRoot(), 'bin', 'flutter');
|
||||
@ -96,9 +95,6 @@ abstract class FlutterTestDriver {
|
||||
if (_printDebugOutputToStdOut) {
|
||||
arguments.add('--verbose');
|
||||
}
|
||||
if (pidFile != null) {
|
||||
arguments.addAll(<String>['--pid-file', pidFile.path]);
|
||||
}
|
||||
if (script != null) {
|
||||
arguments.add(script);
|
||||
}
|
||||
@ -467,9 +463,7 @@ class FlutterRunTestDriver extends FlutterTestDriver {
|
||||
bool chrome = false,
|
||||
bool expressionEvaluation = true,
|
||||
bool structuredErrors = false,
|
||||
bool machine = true,
|
||||
bool singleWidgetReloads = false,
|
||||
File pidFile,
|
||||
String script,
|
||||
List<String> additionalCommandArgs,
|
||||
}) async {
|
||||
@ -478,7 +472,7 @@ class FlutterRunTestDriver extends FlutterTestDriver {
|
||||
'run',
|
||||
if (!chrome)
|
||||
'--disable-service-auth-codes',
|
||||
if (machine) '--machine',
|
||||
'--machine',
|
||||
if (!spawnDdsInstance) '--disable-dds',
|
||||
...getLocalEngineArguments(),
|
||||
'-d',
|
||||
@ -497,7 +491,6 @@ class FlutterRunTestDriver extends FlutterTestDriver {
|
||||
withDebugger: withDebugger,
|
||||
startPaused: startPaused,
|
||||
pauseOnExceptions: pauseOnExceptions,
|
||||
pidFile: pidFile,
|
||||
script: script,
|
||||
singleWidgetReloads: singleWidgetReloads,
|
||||
);
|
||||
@ -508,7 +501,6 @@ class FlutterRunTestDriver extends FlutterTestDriver {
|
||||
bool withDebugger = false,
|
||||
bool startPaused = false,
|
||||
bool pauseOnExceptions = false,
|
||||
File pidFile,
|
||||
bool singleWidgetReloads = false,
|
||||
List<String> additionalCommandArgs,
|
||||
}) async {
|
||||
@ -529,7 +521,6 @@ class FlutterRunTestDriver extends FlutterTestDriver {
|
||||
withDebugger: withDebugger,
|
||||
startPaused: startPaused,
|
||||
pauseOnExceptions: pauseOnExceptions,
|
||||
pidFile: pidFile,
|
||||
singleWidgetReloads: singleWidgetReloads,
|
||||
attachPort: port,
|
||||
);
|
||||
@ -543,7 +534,6 @@ class FlutterRunTestDriver extends FlutterTestDriver {
|
||||
bool startPaused = false,
|
||||
bool pauseOnExceptions = false,
|
||||
bool singleWidgetReloads = false,
|
||||
File pidFile,
|
||||
int attachPort,
|
||||
}) async {
|
||||
assert(!startPaused || withDebugger);
|
||||
@ -551,7 +541,6 @@ class FlutterRunTestDriver extends FlutterTestDriver {
|
||||
args,
|
||||
script: script,
|
||||
withDebugger: withDebugger,
|
||||
pidFile: pidFile,
|
||||
singleWidgetReloads: singleWidgetReloads,
|
||||
);
|
||||
|
||||
@ -738,7 +727,6 @@ class FlutterTestTestDriver extends FlutterTestDriver {
|
||||
bool withDebugger = false,
|
||||
bool pauseOnExceptions = false,
|
||||
bool coverage = false,
|
||||
File pidFile,
|
||||
Future<void> Function() beforeStart,
|
||||
}) async {
|
||||
await _setupProcess(<String>[
|
||||
@ -748,7 +736,7 @@ class FlutterTestTestDriver extends FlutterTestDriver {
|
||||
'--machine',
|
||||
if (coverage)
|
||||
'--coverage',
|
||||
], script: testFile, withDebugger: withDebugger, pauseOnExceptions: pauseOnExceptions, pidFile: pidFile, beforeStart: beforeStart);
|
||||
], script: testFile, withDebugger: withDebugger, pauseOnExceptions: pauseOnExceptions, beforeStart: beforeStart);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -757,7 +745,6 @@ class FlutterTestTestDriver extends FlutterTestDriver {
|
||||
String script,
|
||||
bool withDebugger = false,
|
||||
bool pauseOnExceptions = false,
|
||||
File pidFile,
|
||||
Future<void> Function() beforeStart,
|
||||
bool singleWidgetReloads = false,
|
||||
}) async {
|
||||
@ -765,7 +752,6 @@ class FlutterTestTestDriver extends FlutterTestDriver {
|
||||
args,
|
||||
script: script,
|
||||
withDebugger: withDebugger,
|
||||
pidFile: pidFile,
|
||||
singleWidgetReloads: singleWidgetReloads,
|
||||
);
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user