Unverified Commit adf45d1e authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

Refactor signal and command line handler from resident runner (#35406)

parent a429991a
......@@ -437,8 +437,8 @@ class AndroidDevice extends Device {
DebuggingOptions debuggingOptions,
Map<String, dynamic> platformArgs,
bool prebuiltApplication = false,
bool usesTerminalUi = true,
bool ipv6 = false,
bool usesTerminalUi = true,
}) async {
if (!await _checkForSupportedAdbVersion() || !await _checkForSupportedAndroidVersion())
return LaunchResult.failed();
......
......@@ -277,7 +277,7 @@ class AttachCommand extends FlutterCommand {
target: targetFile,
debuggingOptions: debuggingOptions,
packagesFilePath: globalResults['packages'],
usesTerminalUI: daemon == null,
usesTerminalUi: daemon == null,
projectRootPath: argResults['project-root'],
dillOutputPath: argResults['output-dill'],
ipv6: usesIpv6,
......@@ -312,7 +312,15 @@ class AttachCommand extends FlutterCommand {
result = await app.runner.waitForAppToFinish();
assert(result != null);
} else {
result = await runner.attach();
final Completer<void> onAppStart = Completer<void>.sync();
unawaited(onAppStart.future.whenComplete(() {
TerminalHandler(runner)
..setupTerminal()
..registerSignalHandlers();
}));
result = await runner.attach(
appStartedCompleter: onAppStart,
);
assert(result != null);
}
if (result != 0) {
......@@ -350,7 +358,7 @@ class HotRunnerFactory {
List<FlutterDevice> devices, {
String target,
DebuggingOptions debuggingOptions,
bool usesTerminalUI = true,
bool usesTerminalUi = true,
bool benchmarkMode = false,
File applicationBinary,
bool hostIsIde = false,
......@@ -364,7 +372,7 @@ class HotRunnerFactory {
devices,
target: target,
debuggingOptions: debuggingOptions,
usesTerminalUI: usesTerminalUI,
usesTerminalUi: usesTerminalUi,
benchmarkMode: benchmarkMode,
applicationBinary: applicationBinary,
hostIsIde: hostIsIde,
......
......@@ -419,7 +419,7 @@ class AppDomain extends Domain {
<FlutterDevice>[flutterDevice],
target: target,
debuggingOptions: options,
usesTerminalUI: false,
usesTerminalUi: false,
applicationBinary: applicationBinary,
projectRootPath: projectRootPath,
packagesFilePath: packagesFilePath,
......@@ -432,8 +432,8 @@ class AppDomain extends Domain {
<FlutterDevice>[flutterDevice],
target: target,
debuggingOptions: options,
usesTerminalUI: false,
applicationBinary: applicationBinary,
usesTerminalUi: false,
ipv6: ipv6,
);
}
......
......@@ -450,8 +450,8 @@ class RunCommand extends RunCommandBase {
applicationBinary: applicationBinaryPath == null
? null
: fs.file(applicationBinaryPath),
stayResident: stayResident,
ipv6: ipv6,
stayResident: stayResident,
);
}
......@@ -463,7 +463,14 @@ class RunCommand extends RunCommandBase {
final Completer<void> appStartedTimeRecorder = Completer<void>.sync();
// This callback can't throw.
unawaited(appStartedTimeRecorder.future.then<void>(
(_) { appStartedTime = systemClock.now(); }
(_) {
appStartedTime = systemClock.now();
if (stayResident) {
TerminalHandler(runner)
..setupTerminal()
..registerSignalHandlers();
}
}
));
final int result = await runner.run(
......@@ -471,8 +478,9 @@ class RunCommand extends RunCommandBase {
route: route,
shouldBuild: !runningWithPrebuiltApplication && argResults['build'],
);
if (result != 0)
if (result != 0) {
throwToolExit(null, exitCode: result);
}
return FlutterCommandResult(
ExitStatus.success,
timingLabelParts: <String>[
......
......@@ -406,8 +406,8 @@ abstract class Device {
DebuggingOptions debuggingOptions,
Map<String, dynamic> platformArgs,
bool prebuiltApplication = false,
bool usesTerminalUi = true,
bool ipv6 = false,
bool usesTerminalUi = true,
});
/// Whether this device implements support for hot reload.
......
......@@ -115,7 +115,7 @@ class FlutterDevice {
/// expressions requested during debugging of the application.
/// This ensures that the reload process follows the normal orchestration of
/// the Flutter Tools and not just the VM internal service.
Future<void> _connect({
Future<void> connect({
ReloadSources reloadSources,
Restart restart,
CompileExpression compileExpression,
......@@ -375,7 +375,7 @@ class FlutterDevice {
platformArgs: platformArgs,
route: route,
prebuiltApplication: prebuiltMode,
usesTerminalUi: hotRunner.usesTerminalUI,
usesTerminalUi: hotRunner.usesTerminalUi,
ipv6: hotRunner.ipv6,
);
......@@ -437,7 +437,7 @@ class FlutterDevice {
platformArgs: platformArgs,
route: route,
prebuiltApplication: prebuiltMode,
usesTerminalUi: coldRunner.usesTerminalUI,
usesTerminalUi: coldRunner.usesTerminalUi,
ipv6: coldRunner.ipv6,
);
......@@ -509,11 +509,12 @@ abstract class ResidentRunner {
this.flutterDevices, {
this.target,
this.debuggingOptions,
this.usesTerminalUI = true,
String projectRootPath,
String packagesFilePath,
this.stayResident,
this.ipv6,
this.usesTerminalUi = true,
this.stayResident = true,
this.hotMode = true,
}) {
_mainPath = findMainDartFile(target);
_projectRootPath = projectRootPath ?? fs.currentDirectory.path;
......@@ -525,11 +526,12 @@ abstract class ResidentRunner {
final List<FlutterDevice> flutterDevices;
final String target;
final DebuggingOptions debuggingOptions;
final bool usesTerminalUI;
final bool usesTerminalUi;
final bool stayResident;
final bool ipv6;
final Completer<int> _finished = Completer<int>();
bool _exited = false;
bool hotMode ;
String _packagesFilePath;
String get packagesFilePath => _packagesFilePath;
String _projectRootPath;
......@@ -601,68 +603,68 @@ abstract class ResidentRunner {
await Future.wait(futures);
}
Future<void> _debugDumpApp() async {
Future<void> debugDumpApp() async {
await refreshViews();
for (FlutterDevice device in flutterDevices)
await device.debugDumpApp();
}
Future<void> _debugDumpRenderTree() async {
Future<void> debugDumpRenderTree() async {
await refreshViews();
for (FlutterDevice device in flutterDevices)
await device.debugDumpRenderTree();
}
Future<void> _debugDumpLayerTree() async {
Future<void> debugDumpLayerTree() async {
await refreshViews();
for (FlutterDevice device in flutterDevices)
await device.debugDumpLayerTree();
}
Future<void> _debugDumpSemanticsTreeInTraversalOrder() async {
Future<void> debugDumpSemanticsTreeInTraversalOrder() async {
await refreshViews();
for (FlutterDevice device in flutterDevices)
await device.debugDumpSemanticsTreeInTraversalOrder();
}
Future<void> _debugDumpSemanticsTreeInInverseHitTestOrder() async {
Future<void> debugDumpSemanticsTreeInInverseHitTestOrder() async {
await refreshViews();
for (FlutterDevice device in flutterDevices)
await device.debugDumpSemanticsTreeInInverseHitTestOrder();
}
Future<void> _debugToggleDebugPaintSizeEnabled() async {
Future<void> debugToggleDebugPaintSizeEnabled() async {
await refreshViews();
for (FlutterDevice device in flutterDevices)
await device.toggleDebugPaintSizeEnabled();
}
Future<void> _debugToggleDebugCheckElevationsEnabled() async {
Future<void> debugToggleDebugCheckElevationsEnabled() async {
await refreshViews();
for (FlutterDevice device in flutterDevices)
await device.toggleDebugCheckElevationsEnabled();
}
Future<void> _debugTogglePerformanceOverlayOverride() async {
Future<void> debugTogglePerformanceOverlayOverride() async {
await refreshViews();
for (FlutterDevice device in flutterDevices)
await device.debugTogglePerformanceOverlayOverride();
}
Future<void> _debugToggleWidgetInspector() async {
Future<void> debugToggleWidgetInspector() async {
await refreshViews();
for (FlutterDevice device in flutterDevices)
await device.toggleWidgetInspector();
}
Future<void> _debugToggleProfileWidgetBuilds() async {
Future<void> debugToggleProfileWidgetBuilds() async {
await refreshViews();
for (FlutterDevice device in flutterDevices) {
await device.toggleProfileWidgetBuilds();
}
}
Future<void> _screenshot(FlutterDevice device) async {
Future<void> screenshot(FlutterDevice device) async {
final Status status = logger.startProgress('Taking screenshot for ${device.device.name}...', timeout: timeoutConfiguration.fastOperation);
final File outputFile = getUniqueFile(fs.currentDirectory, 'flutter', 'png');
try {
......@@ -700,7 +702,7 @@ abstract class ResidentRunner {
}
}
Future<void> _debugTogglePlatform() async {
Future<void> debugTogglePlatform() async {
await refreshViews();
final String from = await flutterDevices[0].views[0].uiIsolate.flutterPlatformOverride();
String to;
......@@ -709,39 +711,6 @@ abstract class ResidentRunner {
printStatus('Switched operating system to $to');
}
void registerSignalHandlers() {
assert(stayResident);
io.ProcessSignal.SIGINT.watch().listen(_cleanUpAndExit);
io.ProcessSignal.SIGTERM.watch().listen(_cleanUpAndExit);
if (!supportsServiceProtocol || !supportsRestart)
return;
io.ProcessSignal.SIGUSR1.watch().listen(_handleSignal);
io.ProcessSignal.SIGUSR2.watch().listen(_handleSignal);
}
Future<void> _cleanUpAndExit(io.ProcessSignal signal) async {
_resetTerminal();
await cleanupAfterSignal();
io.exit(0);
}
bool _processingUserRequest = false;
Future<void> _handleSignal(io.ProcessSignal signal) async {
if (_processingUserRequest) {
printTrace('Ignoring signal: "$signal" because we are busy.');
return;
}
_processingUserRequest = true;
final bool fullRestart = signal == io.ProcessSignal.SIGUSR2;
try {
await restart(fullRestart: fullRestart);
} finally {
_processingUserRequest = false;
}
}
Future<void> stopEchoingDeviceLog() async {
await Future.wait<void>(
flutterDevices.map<Future<void>>((FlutterDevice device) => device.stopEchoingDeviceLog())
......@@ -764,7 +733,7 @@ abstract class ResidentRunner {
bool viewFound = false;
for (FlutterDevice device in flutterDevices) {
await device._connect(
await device.connect(
reloadSources: reloadSources,
restart: restart,
compileExpression: compileExpression,
......@@ -805,125 +774,6 @@ abstract class ResidentRunner {
return Future<void>.error(error, stack);
}
/// Returns [true] if the input has been handled by this function.
Future<bool> _commonTerminalInputHandler(String character) async {
printStatus(''); // the key the user tapped might be on this line
switch(character) {
case 'a':
if (supportsServiceProtocol) {
await _debugToggleProfileWidgetBuilds();
return true;
}
return false;
case 'd':
case 'D':
await detach();
return true;
case 'h':
case 'H':
case '?':
// help
printHelp(details: true);
return true;
case 'i':
case 'I':
if (supportsServiceProtocol) {
await _debugToggleWidgetInspector();
return true;
}
return false;
case 'L':
if (supportsServiceProtocol) {
await _debugDumpLayerTree();
return true;
}
return false;
case 'o':
case 'O':
if (supportsServiceProtocol && isRunningDebug) {
await _debugTogglePlatform();
return true;
}
return false;
case 'p':
if (supportsServiceProtocol && isRunningDebug) {
await _debugToggleDebugPaintSizeEnabled();
return true;
}
return false;
case 'P':
if (supportsServiceProtocol) {
await _debugTogglePerformanceOverlayOverride();
return true;
}
return false;
case 'q':
case 'Q':
// exit
await exit();
return true;
case 's':
for (FlutterDevice device in flutterDevices) {
if (device.device.supportsScreenshot)
await _screenshot(device);
}
return true;
case 'S':
if (supportsServiceProtocol) {
await _debugDumpSemanticsTreeInTraversalOrder();
return true;
}
return false;
case 't':
case 'T':
if (supportsServiceProtocol) {
await _debugDumpRenderTree();
return true;
}
return false;
case 'U':
if (supportsServiceProtocol) {
await _debugDumpSemanticsTreeInInverseHitTestOrder();
return true;
}
return false;
case 'w':
case 'W':
if (supportsServiceProtocol) {
await _debugDumpApp();
return true;
}
return false;
case 'z':
case 'Z':
await _debugToggleDebugCheckElevationsEnabled();
return true;
}
return false;
}
Future<void> processTerminalInput(String command) async {
// When terminal doesn't support line mode, '\n' can sneak into the input.
command = command.trim();
if (_processingUserRequest) {
printTrace('Ignoring terminal input: "$command" because we are busy.');
return;
}
_processingUserRequest = true;
try {
final bool handled = await _commonTerminalInputHandler(command);
if (!handled)
await handleTerminalCommand(command);
} catch (error, st) {
printError('$error\n$st');
await _cleanUpAndExit(null);
} finally {
_processingUserRequest = false;
}
}
void _serviceDisconnected() {
if (_exited) {
// User requested the application exit.
......@@ -932,7 +782,6 @@ abstract class ResidentRunner {
if (_finished.isCompleted)
return;
printStatus('Lost connection to device.');
_resetTerminal();
_finished.complete(0);
}
......@@ -940,27 +789,9 @@ abstract class ResidentRunner {
if (_finished.isCompleted)
return;
printStatus('Application finished.');
_resetTerminal();
_finished.complete(0);
}
void _resetTerminal() {
if (usesTerminalUI)
terminal.singleCharMode = false;
}
void setupTerminal() {
assert(stayResident);
if (usesTerminalUI) {
if (!logger.quiet) {
printStatus('');
printHelp(details: false);
}
terminal.singleCharMode = true;
terminal.keystrokes.listen(processTerminalInput);
}
}
Future<int> waitForAppToFinish() async {
final int exitCode = await _finished.future;
assert(exitCode != null);
......@@ -1004,10 +835,38 @@ abstract class ResidentRunner {
/// Called when a signal has requested we exit.
Future<void> cleanupAfterSignal();
/// Called right before we exit.
Future<void> cleanupAtFinish();
/// Called when the runner should handle a terminal command.
Future<void> handleTerminalCommand(String code);
Future<void> handleTerminalCommand(String code) async {
switch (code) {
case 'r':
final OperationResult result = await restart(fullRestart: false);
if (!result.isOk) {
printStatus('Try again after fixing the above error(s).', emphasis: true);
}
return;
case 'R':
// If hot restart is not supported for all devices, ignore the command.
if (!canHotRestart) {
return;
}
final OperationResult result = await restart(fullRestart: true);
if (!result.isOk) {
printStatus('Try again after fixing the above error(s).', emphasis: true);
}
return;
case 'l':
case 'L':
final List<FlutterView> views = flutterDevices.expand((FlutterDevice d) => d.views).toList();
printStatus('Connected ${pluralize('view', views.length)}:');
for (FlutterView v in views) {
printStatus('${v.uiIsolate.name} (${v.uiIsolate.id})', indent: 2);
}
}
}
}
class OperationResult {
......@@ -1051,6 +910,176 @@ Future<String> getMissingPackageHintForPlatform(TargetPlatform platform) async {
}
}
/// Redirects terminal commands to the correct resident runner methods.
class TerminalHandler {
TerminalHandler(this.residentRunner);
final ResidentRunner residentRunner;
bool _processingUserRequest = false;
StreamSubscription<void> subscription;
void setupTerminal() {
if (!logger.quiet) {
printStatus('');
residentRunner.printHelp(details: false);
}
terminal.singleCharMode = true;
subscription = terminal.keystrokes.listen(processTerminalInput);
}
void registerSignalHandlers() {
assert(residentRunner.stayResident);
io.ProcessSignal.SIGINT.watch().listen(_cleanUpAndExit);
io.ProcessSignal.SIGTERM.watch().listen(_cleanUpAndExit);
if (!residentRunner.supportsServiceProtocol || !residentRunner.supportsRestart)
return;
io.ProcessSignal.SIGUSR1.watch().listen(_handleSignal);
io.ProcessSignal.SIGUSR2.watch().listen(_handleSignal);
}
/// Returns [true] if the input has been handled by this function.
Future<bool> _commonTerminalInputHandler(String character) async {
printStatus(''); // the key the user tapped might be on this line
switch(character) {
case 'a':
if (residentRunner.supportsServiceProtocol) {
await residentRunner.debugToggleProfileWidgetBuilds();
return true;
}
return false;
case 'd':
case 'D':
await residentRunner.detach();
return true;
case 'h':
case 'H':
case '?':
// help
residentRunner.printHelp(details: true);
return true;
case 'i':
case 'I':
if (residentRunner.supportsServiceProtocol) {
await residentRunner.debugToggleWidgetInspector();
return true;
}
return false;
case 'L':
if (residentRunner.supportsServiceProtocol) {
await residentRunner.debugDumpLayerTree();
return true;
}
return false;
case 'o':
case 'O':
if (residentRunner.supportsServiceProtocol && residentRunner.isRunningDebug) {
await residentRunner.debugTogglePlatform();
return true;
}
return false;
case 'p':
if (residentRunner.supportsServiceProtocol && residentRunner.isRunningDebug) {
await residentRunner.debugToggleDebugPaintSizeEnabled();
return true;
}
return false;
case 'P':
if (residentRunner.supportsServiceProtocol) {
await residentRunner.debugTogglePerformanceOverlayOverride();
return true;
}
return false;
case 'q':
case 'Q':
// exit
await residentRunner.exit();
return true;
case 's':
for (FlutterDevice device in residentRunner.flutterDevices) {
if (device.device.supportsScreenshot)
await residentRunner.screenshot(device);
}
return true;
case 'S':
if (residentRunner.supportsServiceProtocol) {
await residentRunner.debugDumpSemanticsTreeInTraversalOrder();
return true;
}
return false;
case 't':
case 'T':
if (residentRunner.supportsServiceProtocol) {
await residentRunner.debugDumpRenderTree();
return true;
}
return false;
case 'U':
if (residentRunner.supportsServiceProtocol) {
await residentRunner.debugDumpSemanticsTreeInInverseHitTestOrder();
return true;
}
return false;
case 'w':
case 'W':
if (residentRunner.supportsServiceProtocol) {
await residentRunner.debugDumpApp();
return true;
}
return false;
case 'z':
case 'Z':
await residentRunner.debugToggleDebugCheckElevationsEnabled();
return true;
}
return false;
}
Future<void> processTerminalInput(String command) async {
// When terminal doesn't support line mode, '\n' can sneak into the input.
command = command.trim();
if (_processingUserRequest) {
printTrace('Ignoring terminal input: "$command" because we are busy.');
return;
}
_processingUserRequest = true;
try {
final bool handled = await _commonTerminalInputHandler(command);
if (!handled)
await residentRunner.handleTerminalCommand(command);
} catch (error, st) {
printError('$error\n$st');
await _cleanUpAndExit(null);
} finally {
_processingUserRequest = false;
}
}
Future<void> _handleSignal(io.ProcessSignal signal) async {
if (_processingUserRequest) {
printTrace('Ignoring signal: "$signal" because we are busy.');
return;
}
_processingUserRequest = true;
final bool fullRestart = signal == io.ProcessSignal.SIGUSR2;
try {
await residentRunner.restart(fullRestart: fullRestart);
} finally {
_processingUserRequest = false;
}
}
Future<void> _cleanUpAndExit(io.ProcessSignal signal) async {
terminal.singleCharMode = false;
await subscription.cancel();
await residentRunner.cleanupAfterSignal();
io.exit(0);
}
}
class DebugConnectionInfo {
DebugConnectionInfo({ this.httpUri, this.wsUri, this.baseUri });
......
......@@ -37,10 +37,10 @@ class ResidentWebRunner extends ResidentRunner {
}) : super(
flutterDevices,
target: target,
usesTerminalUI: true,
stayResident: true,
debuggingOptions: debuggingOptions,
ipv6: ipv6,
usesTerminalUi: true,
stayResident: true,
);
WebAssetServer _server;
......@@ -54,7 +54,6 @@ class ResidentWebRunner extends ResidentRunner {
{Completer<DebugConnectionInfo> connectionInfoCompleter,
Completer<void> appStartedCompleter}) async {
connectionInfoCompleter?.complete(DebugConnectionInfo());
setupTerminal();
final int result = await waitForAppToFinish();
await cleanupAtFinish();
return result;
......
......@@ -19,16 +19,17 @@ class ColdRunner extends ResidentRunner {
List<FlutterDevice> devices, {
String target,
DebuggingOptions debuggingOptions,
bool usesTerminalUI = true,
this.traceStartup = false,
this.awaitFirstFrameWhenTracing = true,
this.applicationBinary,
bool stayResident = true,
bool ipv6 = false,
bool usesTerminalUi = false,
bool stayResident = true,
}) : super(devices,
target: target,
debuggingOptions: debuggingOptions,
usesTerminalUI: usesTerminalUI,
hotMode: false,
usesTerminalUi: usesTerminalUi,
stayResident: stayResident,
ipv6: ipv6);
......@@ -104,9 +105,6 @@ class ColdRunner extends ResidentRunner {
);
}
appFinished();
} else if (stayResident) {
setupTerminal();
registerSignalHandlers();
}
appStartedCompleter?.complete();
......@@ -138,10 +136,6 @@ class ColdRunner extends ResidentRunner {
printTrace('Connected to $view.');
}
}
if (stayResident) {
setupTerminal();
registerSignalHandlers();
}
appStartedCompleter?.complete();
if (stayResident) {
return waitForAppToFinish();
......@@ -150,9 +144,6 @@ class ColdRunner extends ResidentRunner {
return 0;
}
@override
Future<void> handleTerminalCommand(String code) async { }
@override
Future<void> cleanupAfterSignal() async {
await stopEchoingDeviceLog();
......
......@@ -57,7 +57,7 @@ class HotRunner extends ResidentRunner {
List<FlutterDevice> devices, {
String target,
DebuggingOptions debuggingOptions,
bool usesTerminalUI = true,
bool usesTerminalUi = true,
this.benchmarkMode = false,
this.applicationBinary,
this.hostIsIde = false,
......@@ -69,10 +69,11 @@ class HotRunner extends ResidentRunner {
}) : super(devices,
target: target,
debuggingOptions: debuggingOptions,
usesTerminalUI: usesTerminalUI,
usesTerminalUi: usesTerminalUi,
projectRootPath: projectRootPath,
packagesFilePath: packagesFilePath,
stayResident: stayResident,
hotMode: true,
ipv6: ipv6);
final bool benchmarkMode;
......@@ -194,11 +195,6 @@ class HotRunner extends ResidentRunner {
printTrace('Connected to $view.');
}
if (stayResident) {
setupTerminal();
registerSignalHandlers();
}
appStartedCompleter?.complete();
if (benchmarkMode) {
......@@ -264,32 +260,6 @@ class HotRunner extends ResidentRunner {
);
}
@override
Future<void> handleTerminalCommand(String code) async {
final String lower = code.toLowerCase();
if (lower == 'r') {
OperationResult result;
if (code == 'R') {
// If hot restart is not supported for all devices, ignore the command.
if (!canHotRestart) {
return;
}
result = await restart(fullRestart: true);
} else {
result = await restart(fullRestart: false);
}
if (!result.isOk) {
printStatus('Try again after fixing the above error(s).', emphasis: true);
}
} else if (lower == 'l') {
final List<FlutterView> views = flutterDevices.expand((FlutterDevice d) => d.views).toList();
printStatus('Connected ${pluralize('view', views.length)}:');
for (FlutterView v in views) {
printStatus('${v.uiIsolate.name} (${v.uiIsolate.id})', indent: 2);
}
}
}
Future<List<Uri>> _initDevFS() async {
final String fsName = fs.path.basename(projectRootPath);
final List<Uri> devFSUris = <Uri>[];
......
......@@ -24,15 +24,18 @@ import '../src/context.dart';
import '../src/mocks.dart';
void main() {
final StreamLogger logger = StreamLogger();
group('attach', () {
final FileSystem testFileSystem = MemoryFileSystem(
style: platform.isWindows ? FileSystemStyle.windows : FileSystemStyle
.posix,
);
StreamLogger logger;
FileSystem testFileSystem;
setUp(() {
Cache.disableLocking();
logger = StreamLogger();
testFileSystem = MemoryFileSystem(
style: platform.isWindows
? FileSystemStyle.windows
: FileSystemStyle.posix,
);
testFileSystem.directory('lib').createSync();
testFileSystem.file(testFileSystem.path.join('lib', 'main.dart')).createSync();
});
......@@ -108,7 +111,8 @@ void main() {
const String outputDill = '/tmp/output.dill';
final MockHotRunner mockHotRunner = MockHotRunner();
when(mockHotRunner.attach()).thenAnswer((_) async => 0);
when(mockHotRunner.attach(appStartedCompleter: anyNamed('appStartedCompleter')))
.thenAnswer((_) async => 0);
final MockHotRunnerFactory mockHotRunnerFactory = MockHotRunnerFactory();
when(
......@@ -119,7 +123,7 @@ void main() {
dillOutputPath: anyNamed('dillOutputPath'),
debuggingOptions: anyNamed('debuggingOptions'),
packagesFilePath: anyNamed('packagesFilePath'),
usesTerminalUI: anyNamed('usesTerminalUI'),
usesTerminalUi: anyNamed('usesTerminalUi'),
flutterProject: anyNamed('flutterProject'),
ipv6: false,
),
......@@ -151,7 +155,7 @@ void main() {
dillOutputPath: outputDill,
debuggingOptions: anyNamed('debuggingOptions'),
packagesFilePath: anyNamed('packagesFilePath'),
usesTerminalUI: anyNamed('usesTerminalUI'),
usesTerminalUi: anyNamed('usesTerminalUi'),
flutterProject: anyNamed('flutterProject'),
ipv6: false,
),
......@@ -219,14 +223,14 @@ void main() {
.thenReturn(<ForwardedPort>[ForwardedPort(hostPort, devicePort)]);
when(portForwarder.unforward(any))
.thenAnswer((_) async => null);
when(mockHotRunner.attach())
.thenAnswer((_) async => 0);
when(mockHotRunner.attach(appStartedCompleter: anyNamed('appStartedCompleter')))
.thenAnswer((_) async => 0);
when(mockHotRunnerFactory.build(
any,
target: anyNamed('target'),
debuggingOptions: anyNamed('debuggingOptions'),
packagesFilePath: anyNamed('packagesFilePath'),
usesTerminalUI: anyNamed('usesTerminalUI'),
usesTerminalUi: anyNamed('usesTerminalUi'),
flutterProject: anyNamed('flutterProject'),
ipv6: false,
)).thenReturn(mockHotRunner);
......@@ -256,7 +260,7 @@ void main() {
target: foo.path,
debuggingOptions: anyNamed('debuggingOptions'),
packagesFilePath: anyNamed('packagesFilePath'),
usesTerminalUI: anyNamed('usesTerminalUI'),
usesTerminalUi: anyNamed('usesTerminalUi'),
flutterProject: anyNamed('flutterProject'),
ipv6: false,
)).called(1);
......
......@@ -3,91 +3,114 @@
// found in the LICENSE file.
import 'dart:async';
import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/devfs.dart';
import 'package:flutter_tools/src/device.dart';
import 'package:flutter_tools/src/resident_runner.dart';
import 'package:flutter_tools/src/run_hot.dart';
import 'package:flutter_tools/src/vmservice.dart';
import 'package:mockito/mockito.dart';
import 'src/common.dart';
import 'src/context.dart';
class TestRunner extends ResidentRunner {
TestRunner(List<FlutterDevice> devices)
: super(devices);
bool hasHelpBeenPrinted = false;
String receivedCommand;
@override
Future<void> cleanupAfterSignal() async { }
import 'src/testbed.dart';
@override
Future<void> cleanupAtFinish() async { }
@override
Future<void> handleTerminalCommand(String code) async {
receivedCommand = code;
}
void main() {
group('ResidentRunner', () {
final Uri testUri = Uri.parse('foo://bar');
Testbed testbed;
MockDevice mockDevice;
MockVMService mockVMService;
MockDevFS mockDevFS;
ResidentRunner residentRunner;
@override
void printHelp({ bool details }) {
hasHelpBeenPrinted = true;
}
setUp(() {
testbed = Testbed(setup: () {
residentRunner = HotRunner(
<FlutterDevice>[
mockDevice,
],
stayResident: false,
debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
);
});
mockDevice = MockDevice();
mockVMService = MockVMService();
mockDevFS = MockDevFS();
// DevFS Mocks
when(mockDevFS.lastCompiled).thenReturn(DateTime(2000));
when(mockDevFS.sources).thenReturn(<Uri>[]);
when(mockDevFS.destroy()).thenAnswer((Invocation invocation) async { });
// FlutterDevice Mocks.
when(mockDevice.updateDevFS(
// Intentionally provide empty list to match above mock.
invalidatedFiles: <Uri>[],
mainPath: anyNamed('mainPath'),
target: anyNamed('target'),
bundle: anyNamed('bundle'),
firstBuildTime: anyNamed('firstBuildTime'),
bundleFirstUpload: anyNamed('bundleFirstUpload'),
bundleDirty: anyNamed('bundleDirty'),
fullRestart: anyNamed('fullRestart'),
projectRootPath: anyNamed('projectRootPath'),
pathToReload: anyNamed('pathToReload'),
)).thenAnswer((Invocation invocation) async {
return UpdateFSReport(
success: true,
syncedBytes: 0,
invalidatedSourcesCount: 0,
);
});
when(mockDevice.devFS).thenReturn(mockDevFS);
when(mockDevice.views).thenReturn(<FlutterView>[
MockFlutterView(),
]);
when(mockDevice.stopEchoingDeviceLog()).thenAnswer((Invocation invocation) async { });
when(mockDevice.observatoryUris).thenReturn(<Uri>[
testUri,
]);
when(mockDevice.connect(
reloadSources: anyNamed('reloadSources'),
restart: anyNamed('restart'),
compileExpression: anyNamed('compileExpression')
)).thenAnswer((Invocation invocation) async { });
when(mockDevice.setupDevFS(any, any, packagesFilePath: anyNamed('packagesFilePath')))
.thenAnswer((Invocation invocation) async {
return testUri;
});
when(mockDevice.vmServices).thenReturn(<VMService>[
mockVMService,
]);
when(mockDevice.refreshViews()).thenAnswer((Invocation invocation) async { });
// VMService mocks.
when(mockVMService.wsAddress).thenReturn(testUri);
when(mockVMService.done).thenAnswer((Invocation invocation) {
final Completer<void> result = Completer<void>.sync();
return result.future;
});
});
@override
Future<int> run({
Completer<DebugConnectionInfo> connectionInfoCompleter,
Completer<void> appStartedCompleter,
String route,
bool shouldBuild = true,
}) async => null;
test('Can attach to device successfully', () => testbed.run(() async {
final Completer<DebugConnectionInfo> onConnectionInfo = Completer<DebugConnectionInfo>.sync();
final Completer<void> onAppStart = Completer<void>.sync();
final Future<int> result = residentRunner.attach(
appStartedCompleter: onAppStart,
connectionInfoCompleter: onConnectionInfo,
);
final Future<DebugConnectionInfo> connectionInfo = onConnectionInfo.future;
@override
Future<int> attach({
Completer<DebugConnectionInfo> connectionInfoCompleter,
Completer<void> appStartedCompleter,
}) async => null;
}
expect(await result, 0);
void main() {
TestRunner createTestRunner() {
// TODO(jacobr): make these tests run with `trackWidgetCreation: true` as
// well as the default flags.
return TestRunner(
<FlutterDevice>[FlutterDevice(MockDevice(), trackWidgetCreation: false, buildMode: BuildMode.debug)],
);
}
verify(mockDevice.initLogReader()).called(1);
group('keyboard input handling', () {
testUsingContext('single help character', () async {
final TestRunner testRunner = createTestRunner();
expect(testRunner.hasHelpBeenPrinted, isFalse);
await testRunner.processTerminalInput('h');
expect(testRunner.hasHelpBeenPrinted, isTrue);
});
testUsingContext('help character surrounded with newlines', () async {
final TestRunner testRunner = createTestRunner();
expect(testRunner.hasHelpBeenPrinted, isFalse);
await testRunner.processTerminalInput('\nh\n');
expect(testRunner.hasHelpBeenPrinted, isTrue);
});
testUsingContext('reload character with trailing newline', () async {
final TestRunner testRunner = createTestRunner();
expect(testRunner.receivedCommand, isNull);
await testRunner.processTerminalInput('r\n');
expect(testRunner.receivedCommand, equals('r'));
});
testUsingContext('newlines', () async {
final TestRunner testRunner = createTestRunner();
expect(testRunner.receivedCommand, isNull);
await testRunner.processTerminalInput('\n\n');
expect(testRunner.receivedCommand, equals(''));
});
expect(onConnectionInfo.isCompleted, true);
expect((await connectionInfo).baseUri, 'foo://bar');
expect(onAppStart.isCompleted, true);
}));
});
}
class MockDevice extends Mock implements Device {
MockDevice() {
when(isSupported()).thenReturn(true);
}
}
class MockDevice extends Mock implements FlutterDevice {}
class MockFlutterView extends Mock implements FlutterView {}
class MockVMService extends Mock implements VMService {}
class MockDevFS extends Mock implements DevFS {}
// Copyright 2017 The Chromium 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 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/device.dart';
import 'package:flutter_tools/src/resident_runner.dart';
import 'package:mockito/mockito.dart';
import 'src/common.dart';
import 'src/context.dart';
void main() {
TestRunner createTestRunner() {
// TODO(jacobr): make these tests run with `trackWidgetCreation: true` as
// well as the default flags.
return TestRunner(
<FlutterDevice>[FlutterDevice(MockDevice(), trackWidgetCreation: false, buildMode: BuildMode.debug)],
);
}
group('keyboard input handling', () {
testUsingContext('single help character', () async {
final TestRunner testRunner = createTestRunner();
final TerminalHandler terminalHandler = TerminalHandler(testRunner);
expect(testRunner.hasHelpBeenPrinted, isFalse);
await terminalHandler.processTerminalInput('h');
expect(testRunner.hasHelpBeenPrinted, isTrue);
});
testUsingContext('help character surrounded with newlines', () async {
final TestRunner testRunner = createTestRunner();
final TerminalHandler terminalHandler = TerminalHandler(testRunner);
expect(testRunner.hasHelpBeenPrinted, isFalse);
await terminalHandler.processTerminalInput('\nh\n');
expect(testRunner.hasHelpBeenPrinted, isTrue);
});
testUsingContext('reload character with trailing newline', () async {
final TestRunner testRunner = createTestRunner();
final TerminalHandler terminalHandler = TerminalHandler(testRunner);
expect(testRunner.receivedCommand, isNull);
await terminalHandler.processTerminalInput('r\n');
expect(testRunner.receivedCommand, equals('r'));
});
testUsingContext('newlines', () async {
final TestRunner testRunner = createTestRunner();
final TerminalHandler terminalHandler = TerminalHandler(testRunner);
expect(testRunner.receivedCommand, isNull);
await terminalHandler.processTerminalInput('\n\n');
expect(testRunner.receivedCommand, equals(''));
});
});
group('keycode verification, brought to you by the letter r', () {
MockResidentRunner mockResidentRunner;
TerminalHandler terminalHandler;
setUp(() {
mockResidentRunner = MockResidentRunner();
terminalHandler = TerminalHandler(mockResidentRunner);
when(mockResidentRunner.supportsServiceProtocol).thenReturn(true);
when(mockResidentRunner.handleTerminalCommand(any)).thenReturn(null);
});
testUsingContext('a - debugToggleProfileWidgetBuilds with service protocol', () async {
await terminalHandler.processTerminalInput('a');
verify(mockResidentRunner.debugToggleProfileWidgetBuilds()).called(1);
});
testUsingContext('a - debugToggleProfileWidgetBuilds without service protocol', () async {
when(mockResidentRunner.supportsServiceProtocol).thenReturn(false);
await terminalHandler.processTerminalInput('a');
verifyNever(mockResidentRunner.debugToggleProfileWidgetBuilds());
});
testUsingContext('a - debugToggleProfileWidgetBuilds', () async {
when(mockResidentRunner.supportsServiceProtocol).thenReturn(true);
await terminalHandler.processTerminalInput('a');
verify(mockResidentRunner.debugToggleProfileWidgetBuilds()).called(1);
});
testUsingContext('d,D - detach', () async {
await terminalHandler.processTerminalInput('d');
await terminalHandler.processTerminalInput('D');
verify(mockResidentRunner.detach()).called(2);
});
testUsingContext('h,H,? - printHelp', () async {
await terminalHandler.processTerminalInput('h');
await terminalHandler.processTerminalInput('H');
await terminalHandler.processTerminalInput('?');
verify(mockResidentRunner.printHelp(details: true)).called(3);
});
testUsingContext('i, I - debugToggleWidgetInspector with service protocol', () async {
await terminalHandler.processTerminalInput('i');
await terminalHandler.processTerminalInput('I');
verify(mockResidentRunner.debugToggleWidgetInspector()).called(2);
});
testUsingContext('i, I - debugToggleWidgetInspector without service protocol', () async {
when(mockResidentRunner.supportsServiceProtocol).thenReturn(false);
await terminalHandler.processTerminalInput('i');
await terminalHandler.processTerminalInput('I');
verifyNever(mockResidentRunner.debugToggleWidgetInspector());
});
testUsingContext('L - debugDumpLayerTree with service protocol', () async {
await terminalHandler.processTerminalInput('L');
verify(mockResidentRunner.debugDumpLayerTree()).called(1);
});
testUsingContext('L - debugDumpLayerTree without service protocol', () async {
when(mockResidentRunner.supportsServiceProtocol).thenReturn(false);
await terminalHandler.processTerminalInput('L');
verifyNever(mockResidentRunner.debugDumpLayerTree());
});
testUsingContext('o,O - debugTogglePlatform with service protocol and debug mode', () async {
when(mockResidentRunner.isRunningDebug).thenReturn(true);
await terminalHandler.processTerminalInput('o');
await terminalHandler.processTerminalInput('O');
verify(mockResidentRunner.debugTogglePlatform()).called(2);
});
testUsingContext('o,O - debugTogglePlatform without service protocol', () async {
when(mockResidentRunner.supportsServiceProtocol).thenReturn(false);
when(mockResidentRunner.isRunningDebug).thenReturn(true);
await terminalHandler.processTerminalInput('o');
await terminalHandler.processTerminalInput('O');
verifyNever(mockResidentRunner.debugTogglePlatform());
});
testUsingContext('p - debugToggleDebugPaintSizeEnabled with service protocol and debug mode', () async {
when(mockResidentRunner.isRunningDebug).thenReturn(true);
await terminalHandler.processTerminalInput('p');
verify(mockResidentRunner.debugToggleDebugPaintSizeEnabled()).called(1);
});
testUsingContext('p - debugTogglePlatform without service protocol', () async {
when(mockResidentRunner.supportsServiceProtocol).thenReturn(false);
when(mockResidentRunner.isRunningDebug).thenReturn(true);
await terminalHandler.processTerminalInput('p');
verifyNever(mockResidentRunner.debugToggleDebugPaintSizeEnabled());
});
testUsingContext('p - debugToggleDebugPaintSizeEnabled with service protocol and debug mode', () async {
when(mockResidentRunner.isRunningDebug).thenReturn(true);
await terminalHandler.processTerminalInput('p');
verify(mockResidentRunner.debugToggleDebugPaintSizeEnabled()).called(1);
});
testUsingContext('p - debugTogglePlatform without service protocol', () async {
when(mockResidentRunner.supportsServiceProtocol).thenReturn(false);
when(mockResidentRunner.isRunningDebug).thenReturn(true);
await terminalHandler.processTerminalInput('p');
verifyNever(mockResidentRunner.debugToggleDebugPaintSizeEnabled());
});
testUsingContext('P - debugTogglePerformanceOverlayOverride with service protocol', () async {
await terminalHandler.processTerminalInput('P');
verify(mockResidentRunner.debugTogglePerformanceOverlayOverride()).called(1);
});
testUsingContext('P - debugTogglePerformanceOverlayOverride without service protocol', () async {
when(mockResidentRunner.supportsServiceProtocol).thenReturn(false);
await terminalHandler.processTerminalInput('P');
verifyNever(mockResidentRunner.debugTogglePerformanceOverlayOverride());
});
testUsingContext('q,Q - exit', () async {
await terminalHandler.processTerminalInput('q');
await terminalHandler.processTerminalInput('Q');
verify(mockResidentRunner.exit()).called(2);
});
testUsingContext('s - screenshot', () async {
final MockDevice mockDevice = MockDevice();
final MockFlutterDevice mockFlutterDevice = MockFlutterDevice();
when(mockResidentRunner.isRunningDebug).thenReturn(true);
when(mockResidentRunner.flutterDevices).thenReturn(<FlutterDevice>[mockFlutterDevice]);
when(mockFlutterDevice.device).thenReturn(mockDevice);
when(mockDevice.supportsScreenshot).thenReturn(true);
await terminalHandler.processTerminalInput('s');
verify(mockResidentRunner.screenshot(mockFlutterDevice)).called(1);
});
testUsingContext('S - debugDumpSemanticsTreeInTraversalOrder with service protocol', () async {
await terminalHandler.processTerminalInput('S');
verify(mockResidentRunner.debugDumpSemanticsTreeInTraversalOrder()).called(1);
});
testUsingContext('S - debugDumpSemanticsTreeInTraversalOrder without service protocol', () async {
when(mockResidentRunner.supportsServiceProtocol).thenReturn(false);
await terminalHandler.processTerminalInput('S');
verifyNever(mockResidentRunner.debugDumpSemanticsTreeInTraversalOrder());
});
testUsingContext('t,T - debugDumpRenderTree with service protocol', () async {
await terminalHandler.processTerminalInput('t');
await terminalHandler.processTerminalInput('T');
verify(mockResidentRunner.debugDumpRenderTree()).called(2);
});
testUsingContext('t,T - debugDumpSemanticsTreeInTraversalOrder without service protocol', () async {
when(mockResidentRunner.supportsServiceProtocol).thenReturn(false);
await terminalHandler.processTerminalInput('t');
await terminalHandler.processTerminalInput('T');
verifyNever(mockResidentRunner.debugDumpRenderTree());
});
testUsingContext('U - debugDumpRenderTree with service protocol', () async {
await terminalHandler.processTerminalInput('U');
verify(mockResidentRunner.debugDumpSemanticsTreeInInverseHitTestOrder()).called(1);
});
testUsingContext('U - debugDumpSemanticsTreeInTraversalOrder without service protocol', () async {
when(mockResidentRunner.supportsServiceProtocol).thenReturn(false);
await terminalHandler.processTerminalInput('U');
verifyNever(mockResidentRunner.debugDumpSemanticsTreeInInverseHitTestOrder());
});
testUsingContext('w,W - debugDumpApp with service protocol', () async {
await terminalHandler.processTerminalInput('w');
await terminalHandler.processTerminalInput('W');
verify(mockResidentRunner.debugDumpApp()).called(2);
});
testUsingContext('w,W - debugDumpApp without service protocol', () async {
when(mockResidentRunner.supportsServiceProtocol).thenReturn(false);
await terminalHandler.processTerminalInput('w');
await terminalHandler.processTerminalInput('W');
verifyNever(mockResidentRunner.debugDumpApp());
});
testUsingContext('z,Z - debugToggleDebugCheckElevationsEnabled with service protocol', () async {
await terminalHandler.processTerminalInput('z');
await terminalHandler.processTerminalInput('Z');
verify(mockResidentRunner.debugToggleDebugCheckElevationsEnabled()).called(2);
});
testUsingContext('z,Z - debugToggleDebugCheckElevationsEnabled without service protocol', () async {
when(mockResidentRunner.supportsServiceProtocol).thenReturn(false);
await terminalHandler.processTerminalInput('z');
await terminalHandler.processTerminalInput('Z');
// This should probably be disable when the service protocol is not enabled.
verify(mockResidentRunner.debugToggleDebugCheckElevationsEnabled()).called(2);
});
});
}
class MockDevice extends Mock implements Device {
MockDevice() {
when(isSupported()).thenReturn(true);
}
}
class MockResidentRunner extends Mock implements ResidentRunner {}
class MockFlutterDevice extends Mock implements FlutterDevice {}
class TestRunner extends ResidentRunner {
TestRunner(List<FlutterDevice> devices)
: super(devices);
bool hasHelpBeenPrinted = false;
String receivedCommand;
@override
Future<void> cleanupAfterSignal() async { }
@override
Future<void> cleanupAtFinish() async { }
@override
Future<void> handleTerminalCommand(String code) async {
receivedCommand = code;
}
@override
void printHelp({ bool details }) {
hasHelpBeenPrinted = true;
}
@override
Future<int> run({
Completer<DebugConnectionInfo> connectionInfoCompleter,
Completer<void> appStartedCompleter,
String route,
bool shouldBuild = true,
}) async => null;
@override
Future<int> attach({
Completer<DebugConnectionInfo> connectionInfoCompleter,
Completer<void> appStartedCompleter,
}) async => null;
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment