blob: 6eccf1abf6a58a31e781179874f396174307ec48 [file] [log] [blame]
// 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.
// @dart = 2.8
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:dwds/dwds.dart';
import 'package:file/memory.dart';
import 'package:flutter_tools/src/application_package.dart';
import 'package:flutter_tools/src/asset.dart';
import 'package:flutter_tools/src/base/dds.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/base/time.dart';
import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/compile.dart';
import 'package:flutter_tools/src/devfs.dart';
import 'package:flutter_tools/src/device.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:flutter_tools/src/isolated/devfs_web.dart';
import 'package:flutter_tools/src/isolated/resident_web_runner.dart';
import 'package:flutter_tools/src/project.dart';
import 'package:flutter_tools/src/reporting/reporting.dart';
import 'package:flutter_tools/src/resident_devtools_handler.dart';
import 'package:flutter_tools/src/resident_runner.dart';
import 'package:flutter_tools/src/vmservice.dart';
import 'package:flutter_tools/src/web/chrome.dart';
import 'package:flutter_tools/src/web/web_device.dart';
import 'package:meta/meta.dart';
import 'package:package_config/package_config.dart';
import 'package:package_config/package_config_types.dart';
import 'package:test/fake.dart';
import 'package:vm_service/vm_service.dart' as vm_service;
import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';
import '../src/common.dart';
import '../src/context.dart';
import '../src/fake_vm_services.dart';
const List<VmServiceExpectation> kAttachLogExpectations = <VmServiceExpectation>[
FakeVmServiceRequest(
method: 'streamListen',
args: <String, Object>{
'streamId': 'Stdout',
},
),
FakeVmServiceRequest(
method: 'streamListen',
args: <String, Object>{
'streamId': 'Stderr',
},
),
];
const List<VmServiceExpectation> kAttachIsolateExpectations = <VmServiceExpectation>[
FakeVmServiceRequest(
method: 'streamListen',
args: <String, Object>{
'streamId': 'Isolate',
}
),
FakeVmServiceRequest(
method: 'registerService',
args: <String, Object>{
'service': 'reloadSources',
'alias': 'Flutter Tools',
}
),
FakeVmServiceRequest(
method: 'registerService',
args: <String, Object>{
'service': 'flutterVersion',
'alias': 'Flutter Tools',
}
),
FakeVmServiceRequest(
method: 'registerService',
args: <String, Object>{
'service': 'flutterMemoryInfo',
'alias': 'Flutter Tools',
}
),
FakeVmServiceRequest(
method: 'streamListen',
args: <String, Object>{
'streamId': 'Extension',
},
),
];
const List<VmServiceExpectation> kAttachExpectations = <VmServiceExpectation>[
...kAttachLogExpectations,
...kAttachIsolateExpectations,
];
void main() {
FakeDebugConnection debugConnection;
FakeChromeDevice chromeDevice;
FakeAppConnection appConnection;
FakeFlutterDevice flutterDevice;
FakeWebDevFS webDevFS;
FakeResidentCompiler residentCompiler;
FakeChromeConnection chromeConnection;
FakeChromeTab chromeTab;
FakeWebServerDevice webServerDevice;
FakeDevice mockDevice;
FakeVmServiceHost fakeVmServiceHost;
FileSystem fileSystem;
ProcessManager processManager;
TestUsage testUsage;
setUp(() {
testUsage = TestUsage();
fileSystem = MemoryFileSystem.test();
processManager = FakeProcessManager.any();
debugConnection = FakeDebugConnection();
mockDevice = FakeDevice();
appConnection = FakeAppConnection();
webDevFS = FakeWebDevFS();
residentCompiler = FakeResidentCompiler();
chromeConnection = FakeChromeConnection();
chromeTab = FakeChromeTab('index.html');
webServerDevice = FakeWebServerDevice();
flutterDevice = FakeFlutterDevice()
.._devFS = webDevFS
..device = mockDevice
..generator = residentCompiler;
fileSystem.file('.packages').writeAsStringSync('\n');
});
void setupMocks() {
fileSystem.file('pubspec.yaml').createSync();
fileSystem.file('lib/main.dart').createSync(recursive: true);
fileSystem.file('web/index.html').createSync(recursive: true);
webDevFS.report = UpdateFSReport(success: true);
debugConnection.fakeVmServiceHost = () => fakeVmServiceHost;
webDevFS.result = ConnectionResult(
appConnection,
debugConnection,
debugConnection.vmService,
);
debugConnection.uri = 'ws://127.0.0.1/abcd/';
chromeConnection.tabs.add(chromeTab);
}
testUsingContext('runner with web server device does not support debugging without --start-paused', () {
final ResidentRunner residentWebRunner = setUpResidentRunner(flutterDevice);
flutterDevice.device = WebServerDevice(
logger: BufferLogger.test(),
);
fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[]);
final ResidentRunner profileResidentWebRunner = ResidentWebRunner(
flutterDevice,
flutterProject: FlutterProject.fromDirectoryTest(fileSystem.currentDirectory),
debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
ipv6: true,
fileSystem: fileSystem,
logger: BufferLogger.test(),
usage: globals.flutterUsage,
systemClock: globals.systemClock,
);
expect(profileResidentWebRunner.debuggingEnabled, false);
flutterDevice.device = FakeChromeDevice();
expect(residentWebRunner.debuggingEnabled, true);
expect(fakeVmServiceHost.hasRemainingExpectations, false);
}, overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
});
testUsingContext('runner with web server device supports debugging with --start-paused', () {
fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[]);
setupMocks();
flutterDevice.device = WebServerDevice(
logger: BufferLogger.test(),
);
final ResidentRunner profileResidentWebRunner = ResidentWebRunner(
flutterDevice,
flutterProject: FlutterProject.fromDirectoryTest(fileSystem.currentDirectory),
debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug, startPaused: true),
ipv6: true,
fileSystem: fileSystem,
logger: BufferLogger.test(),
usage: globals.flutterUsage,
systemClock: globals.systemClock,
);
expect(profileResidentWebRunner.uri, webDevFS.baseUri);
expect(profileResidentWebRunner.debuggingEnabled, true);
}, overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
});
testUsingContext('profile does not supportsServiceProtocol', () {
final ResidentRunner residentWebRunner = ResidentWebRunner(
flutterDevice,
flutterProject: FlutterProject.fromDirectoryTest(fileSystem.currentDirectory),
debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
ipv6: true,
fileSystem: fileSystem,
logger: BufferLogger.test(),
usage: globals.flutterUsage,
systemClock: globals.systemClock,
);
fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[]);
flutterDevice.device = chromeDevice;
final ResidentRunner profileResidentWebRunner = ResidentWebRunner(
flutterDevice,
flutterProject: FlutterProject.fromDirectoryTest(fileSystem.currentDirectory),
debuggingOptions: DebuggingOptions.enabled(BuildInfo.profile),
ipv6: true,
fileSystem: fileSystem,
logger: BufferLogger.test(),
usage: globals.flutterUsage,
systemClock: globals.systemClock,
);
expect(profileResidentWebRunner.supportsServiceProtocol, false);
expect(residentWebRunner.supportsServiceProtocol, true);
}, overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
});
testUsingContext('Can successfully run and connect to vmservice', () async {
final BufferLogger logger = BufferLogger.test();
final ResidentRunner residentWebRunner = setUpResidentRunner(flutterDevice, logger: logger);
fakeVmServiceHost = FakeVmServiceHost(requests: kAttachExpectations.toList());
setupMocks();
final Completer<DebugConnectionInfo> connectionInfoCompleter = Completer<DebugConnectionInfo>();
unawaited(residentWebRunner.run(
connectionInfoCompleter: connectionInfoCompleter,
));
final DebugConnectionInfo debugConnectionInfo = await connectionInfoCompleter.future;
expect(appConnection.ranMain, true);
expect(logger.statusText, contains('Debug service listening on ws://127.0.0.1/abcd/'));
expect(debugConnectionInfo.wsUri.toString(), 'ws://127.0.0.1/abcd/');
}, overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
});
testUsingContext('WebRunner copies compiled app.dill to cache during startup', () async {
final DebuggingOptions debuggingOptions = DebuggingOptions.enabled(
const BuildInfo(BuildMode.debug, null, treeShakeIcons: false),
);
final ResidentRunner residentWebRunner = setUpResidentRunner(flutterDevice, debuggingOptions: debuggingOptions);
fakeVmServiceHost = FakeVmServiceHost(requests: kAttachExpectations.toList());
setupMocks();
residentWebRunner.artifactDirectory.childFile('app.dill').writeAsStringSync('ABC');
final Completer<DebugConnectionInfo> connectionInfoCompleter = Completer<DebugConnectionInfo>();
unawaited(residentWebRunner.run(
connectionInfoCompleter: connectionInfoCompleter,
));
await connectionInfoCompleter.future;
expect(await fileSystem.file(fileSystem.path.join('build', 'cache.dill')).readAsString(), 'ABC');
}, overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
});
testUsingContext('WebRunner copies compiled app.dill to cache during startup with track-widget-creation', () async {
final ResidentRunner residentWebRunner = setUpResidentRunner(flutterDevice);
fakeVmServiceHost = FakeVmServiceHost(requests: kAttachExpectations.toList());
setupMocks();
residentWebRunner.artifactDirectory.childFile('app.dill').writeAsStringSync('ABC');
final Completer<DebugConnectionInfo> connectionInfoCompleter = Completer<DebugConnectionInfo>();
unawaited(residentWebRunner.run(
connectionInfoCompleter: connectionInfoCompleter,
));
await connectionInfoCompleter.future;
expect(await fileSystem.file(fileSystem.path.join('build', 'cache.dill.track.dill')).readAsString(), 'ABC');
}, overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
});
// Regression test for https://github.com/flutter/flutter/issues/60613
testUsingContext('ResidentWebRunner calls appFailedToStart if initial compilation fails', () async {
fakeVmServiceHost = FakeVmServiceHost(requests: kAttachExpectations.toList());
setupMocks();
final ResidentRunner residentWebRunner = setUpResidentRunner(flutterDevice);
fileSystem.file(globals.fs.path.join('lib', 'main.dart'))
.createSync(recursive: true);
webDevFS.report = UpdateFSReport();
expect(await residentWebRunner.run(), 1);
// Completing this future ensures that the daemon can exit correctly.
expect(await residentWebRunner.waitForAppToFinish(), 1);
}, overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
});
testUsingContext('Can successfully run without an index.html including status warning', () async {
final BufferLogger logger = BufferLogger.test();
fakeVmServiceHost = FakeVmServiceHost(requests: kAttachExpectations.toList());
setupMocks();
fileSystem.file(fileSystem.path.join('web', 'index.html'))
.deleteSync();
final ResidentWebRunner residentWebRunner = ResidentWebRunner(
flutterDevice,
flutterProject: FlutterProject.fromDirectoryTest(fileSystem.currentDirectory),
debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
ipv6: true,
stayResident: false,
fileSystem: fileSystem,
logger: logger,
usage: globals.flutterUsage,
systemClock: globals.systemClock,
);
expect(await residentWebRunner.run(), 0);
expect(logger.statusText,
contains('This application is not configured to build on the web'));
}, overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
});
testUsingContext('Can successfully run and disconnect with --no-resident', () async {
fakeVmServiceHost = FakeVmServiceHost(requests: kAttachExpectations.toList());
setupMocks();
final ResidentRunner residentWebRunner = ResidentWebRunner(
flutterDevice,
flutterProject: FlutterProject.fromDirectoryTest(fileSystem.currentDirectory),
debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
ipv6: true,
stayResident: false,
fileSystem: fileSystem,
logger: BufferLogger.test(),
usage: globals.flutterUsage,
systemClock: globals.systemClock,
);
expect(await residentWebRunner.run(), 0);
}, overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
});
testUsingContext('Listens to stdout and stderr streams before running main', () async {
final BufferLogger logger = BufferLogger.test();
final ResidentRunner residentWebRunner = setUpResidentRunner(flutterDevice, logger: logger);
fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
...kAttachLogExpectations,
FakeVmServiceStreamResponse(
streamId: 'Stdout',
event: vm_service.Event(
timestamp: 0,
kind: vm_service.EventStreams.kStdout,
bytes: base64.encode(utf8.encode('THIS MESSAGE IS IMPORTANT'))
),
),
FakeVmServiceStreamResponse(
streamId: 'Stderr',
event: vm_service.Event(
timestamp: 0,
kind: vm_service.EventStreams.kStderr,
bytes: base64.encode(utf8.encode('SO IS THIS'))
),
),
...kAttachIsolateExpectations,
]);
setupMocks();
final Completer<DebugConnectionInfo> connectionInfoCompleter = Completer<DebugConnectionInfo>();
unawaited(residentWebRunner.run(
connectionInfoCompleter: connectionInfoCompleter,
));
await connectionInfoCompleter.future;
expect(logger.statusText, contains('THIS MESSAGE IS IMPORTANT'));
expect(logger.statusText, contains('SO IS THIS'));
}, overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
});
testUsingContext('Listens to extension events with structured errors', () async {
final ResidentRunner residentWebRunner = setUpResidentRunner(flutterDevice, logger: testLogger);
final Map<String, String> extensionData = <String, String>{
'test': 'data',
'renderedErrorText': 'error text',
};
final Map<String, String> emptyExtensionData = <String, String>{
'test': 'data',
'renderedErrorText': '',
};
final Map<String, String> nonStructuredErrorData = <String, String>{
'other': 'other stuff',
};
fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
...kAttachExpectations,
FakeVmServiceStreamResponse(
streamId: 'Extension',
event: vm_service.Event(
timestamp: 0,
extensionKind: 'Flutter.Error',
extensionData: vm_service.ExtensionData.parse(extensionData),
kind: vm_service.EventStreams.kExtension,
),
),
// Empty error text should not break anything.
FakeVmServiceStreamResponse(
streamId: 'Extension',
event: vm_service.Event(
timestamp: 0,
extensionKind: 'Flutter.Error',
extensionData: vm_service.ExtensionData.parse(emptyExtensionData),
kind: vm_service.EventStreams.kExtension,
),
),
// This is not Flutter.Error kind data, so it should not be logged.
FakeVmServiceStreamResponse(
streamId: 'Extension',
event: vm_service.Event(
timestamp: 0,
extensionKind: 'Other',
extensionData: vm_service.ExtensionData.parse(nonStructuredErrorData),
kind: vm_service.EventStreams.kExtension,
),
),
]);
setupMocks();
final Completer<DebugConnectionInfo> connectionInfoCompleter = Completer<DebugConnectionInfo>();
unawaited(residentWebRunner.run(
connectionInfoCompleter: connectionInfoCompleter,
));
await connectionInfoCompleter.future;
await null;
expect(testLogger.statusText, contains('\nerror text'));
expect(testLogger.statusText, isNot(contains('other stuff')));
}, overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
});
testUsingContext('Does not run main with --start-paused', () async {
final ResidentRunner residentWebRunner = ResidentWebRunner(
flutterDevice,
flutterProject: FlutterProject.fromDirectoryTest(fileSystem.currentDirectory),
debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug, startPaused: true),
ipv6: true,
fileSystem: fileSystem,
logger: BufferLogger.test(),
usage: globals.flutterUsage,
systemClock: globals.systemClock,
);
fakeVmServiceHost = FakeVmServiceHost(requests: kAttachExpectations.toList());
setupMocks();
final Completer<DebugConnectionInfo> connectionInfoCompleter = Completer<DebugConnectionInfo>();
unawaited(residentWebRunner.run(
connectionInfoCompleter: connectionInfoCompleter,
));
await connectionInfoCompleter.future;
expect(appConnection.ranMain, false);
}, overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
});
testUsingContext('Can hot reload after attaching', () async {
final BufferLogger logger = BufferLogger.test();
final ResidentRunner residentWebRunner = setUpResidentRunner(
flutterDevice,
logger: logger,
systemClock: SystemClock.fixed(DateTime(2001)),
);
fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
...kAttachExpectations,
const FakeVmServiceRequest(
method: 'hotRestart',
jsonResponse: <String, Object>{
'type': 'Success',
}
),
const FakeVmServiceRequest(
method: 'streamListen',
args: <String, Object>{
'streamId': 'Isolate',
},
),
]);
setupMocks();
final TestChromiumLauncher chromiumLauncher = TestChromiumLauncher();
final Chromium chrome = Chromium(1, chromeConnection, chromiumLauncher: chromiumLauncher);
chromiumLauncher.setInstance(chrome);
flutterDevice.device = GoogleChromeDevice(
fileSystem: fileSystem,
chromiumLauncher: chromiumLauncher,
logger: BufferLogger.test(),
platform: FakePlatform(),
processManager: FakeProcessManager.any(),
);
webDevFS.report = UpdateFSReport(success: true);
final Completer<DebugConnectionInfo> connectionInfoCompleter = Completer<DebugConnectionInfo>();
unawaited(residentWebRunner.run(
connectionInfoCompleter: connectionInfoCompleter,
));
final DebugConnectionInfo debugConnectionInfo = await connectionInfoCompleter.future;
expect(debugConnectionInfo, isNotNull);
final OperationResult result = await residentWebRunner.restart();
expect(logger.statusText, contains('Restarted application in'));
expect(result.code, 0);
expect(webDevFS.mainUri.toString(), contains('entrypoint.dart'));
// ensure that analytics are sent.
expect(testUsage.events, <TestUsageEvent>[
TestUsageEvent('hot', 'restart', parameters: CustomDimensions.fromMap(<String, String>{'cd27': 'web-javascript', 'cd28': '', 'cd29': 'false', 'cd30': 'true', 'cd13': '0', 'cd48': 'false'})),
]);
expect(testUsage.timings, const <TestTimingEvent>[
TestTimingEvent('hot', 'web-incremental-restart', Duration.zero),
]);
}, overrides: <Type, Generator>{
Usage: () => testUsage,
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
});
testUsingContext('Can hot restart after attaching', () async {
final BufferLogger logger = BufferLogger.test();
final ResidentRunner residentWebRunner = setUpResidentRunner(
flutterDevice,
logger: logger,
systemClock: SystemClock.fixed(DateTime(2001)),
);
fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
...kAttachExpectations,
const FakeVmServiceRequest(
method: 'hotRestart',
jsonResponse: <String, Object>{
'type': 'Success',
}
),
]);
setupMocks();
final TestChromiumLauncher chromiumLauncher = TestChromiumLauncher();
final Chromium chrome = Chromium(1, chromeConnection, chromiumLauncher: chromiumLauncher);
chromiumLauncher.setInstance(chrome);
flutterDevice.device = GoogleChromeDevice(
fileSystem: fileSystem,
chromiumLauncher: chromiumLauncher,
logger: BufferLogger.test(),
platform: FakePlatform(),
processManager: FakeProcessManager.any(),
);
webDevFS.report = UpdateFSReport(success: true);
final Completer<DebugConnectionInfo> connectionInfoCompleter = Completer<DebugConnectionInfo>();
unawaited(residentWebRunner.run(
connectionInfoCompleter: connectionInfoCompleter,
));
await connectionInfoCompleter.future;
final OperationResult result = await residentWebRunner.restart(fullRestart: true);
// Ensure that generated entrypoint is generated correctly.
expect(webDevFS.mainUri, isNotNull);
final String entrypointContents = fileSystem.file(webDevFS.mainUri).readAsStringSync();
expect(entrypointContents, contains('// Flutter web bootstrap script'));
expect(entrypointContents, contains("import 'dart:ui' as ui;"));
expect(entrypointContents, contains('await ui.webOnlyWarmupEngine('));
expect(logger.statusText, contains('Restarted application in'));
expect(result.code, 0);
// ensure that analytics are sent.
expect(testUsage.events, <TestUsageEvent>[
TestUsageEvent('hot', 'restart', parameters: CustomDimensions.fromMap(<String, String>{'cd27': 'web-javascript', 'cd28': '', 'cd29': 'false', 'cd30': 'true', 'cd13': '0', 'cd48': 'false'})),
]);
expect(testUsage.timings, const <TestTimingEvent>[
TestTimingEvent('hot', 'web-incremental-restart', Duration.zero),
]);
}, overrides: <Type, Generator>{
Usage: () => testUsage,
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
});
testUsingContext('Can hot restart after attaching with web-server device', () async {
final BufferLogger logger = BufferLogger.test();
final ResidentRunner residentWebRunner = setUpResidentRunner(
flutterDevice,
logger: logger,
systemClock: SystemClock.fixed(DateTime(2001)),
);
fakeVmServiceHost = FakeVmServiceHost(requests :kAttachExpectations);
setupMocks();
flutterDevice.device = webServerDevice;
webDevFS.report = UpdateFSReport(success: true);
final Completer<DebugConnectionInfo> connectionInfoCompleter = Completer<DebugConnectionInfo>();
unawaited(residentWebRunner.run(
connectionInfoCompleter: connectionInfoCompleter,
));
await connectionInfoCompleter.future;
final OperationResult result = await residentWebRunner.restart(fullRestart: true);
expect(logger.statusText, contains('Restarted application in'));
expect(result.code, 0);
// web-server device does not send restart analytics
expect(testUsage.events, isEmpty);
expect(testUsage.timings, isEmpty);
}, overrides: <Type, Generator>{
Usage: () => testUsage,
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
});
testUsingContext('web resident runner is debuggable', () {
final ResidentRunner residentWebRunner = setUpResidentRunner(flutterDevice);
fakeVmServiceHost = FakeVmServiceHost(requests: kAttachExpectations.toList());
expect(residentWebRunner.debuggingEnabled, true);
}, overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
});
testUsingContext('Exits when initial compile fails', () async {
final ResidentRunner residentWebRunner = setUpResidentRunner(flutterDevice);
fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[]);
setupMocks();
webDevFS.report = UpdateFSReport();
final Completer<DebugConnectionInfo> connectionInfoCompleter = Completer<DebugConnectionInfo>();
unawaited(residentWebRunner.run(
connectionInfoCompleter: connectionInfoCompleter,
));
expect(await residentWebRunner.run(), 1);
expect(testUsage.events, isEmpty);
expect(testUsage.timings, isEmpty);
}, overrides: <Type, Generator>{
Usage: () => testUsage,
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
});
testUsingContext('Faithfully displays stdout messages with leading/trailing spaces', () async {
final BufferLogger logger = BufferLogger.test();
final ResidentRunner residentWebRunner = setUpResidentRunner(flutterDevice, logger: logger);
fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
...kAttachLogExpectations,
FakeVmServiceStreamResponse(
streamId: 'Stdout',
event: vm_service.Event(
timestamp: 0,
kind: vm_service.EventStreams.kStdout,
bytes: base64.encode(
utf8.encode(' This is a message with 4 leading and trailing spaces '),
),
),
),
...kAttachIsolateExpectations,
]);
setupMocks();
final Completer<DebugConnectionInfo> connectionInfoCompleter = Completer<DebugConnectionInfo>();
unawaited(residentWebRunner.run(
connectionInfoCompleter: connectionInfoCompleter,
));
await connectionInfoCompleter.future;
expect(logger.statusText,
contains(' This is a message with 4 leading and trailing spaces '));
expect(fakeVmServiceHost.hasRemainingExpectations, false);
}, overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
});
testUsingContext('Fails on compilation errors in hot restart', () async {
final ResidentRunner residentWebRunner = setUpResidentRunner(flutterDevice);
fakeVmServiceHost = FakeVmServiceHost(requests: kAttachExpectations.toList());
setupMocks();
final Completer<DebugConnectionInfo> connectionInfoCompleter = Completer<DebugConnectionInfo>();
unawaited(residentWebRunner.run(
connectionInfoCompleter: connectionInfoCompleter,
));
await connectionInfoCompleter.future;
webDevFS.report = UpdateFSReport();
final OperationResult result = await residentWebRunner.restart(fullRestart: true);
expect(result.code, 1);
expect(result.message, contains('Failed to recompile application.'));
expect(testUsage.events, isEmpty);
expect(testUsage.timings, isEmpty);
}, overrides: <Type, Generator>{
Usage: () => testUsage,
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
});
testUsingContext('Fails non-fatally on vmservice response error for hot restart', () async {
final ResidentRunner residentWebRunner = setUpResidentRunner(flutterDevice);
fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
...kAttachExpectations,
const FakeVmServiceRequest(
method: 'hotRestart',
jsonResponse: <String, Object>{
'type': 'Failed',
},
),
]);
setupMocks();
final Completer<DebugConnectionInfo> connectionInfoCompleter = Completer<DebugConnectionInfo>();
unawaited(residentWebRunner.run(
connectionInfoCompleter: connectionInfoCompleter,
));
await connectionInfoCompleter.future;
final OperationResult result = await residentWebRunner.restart();
expect(result.code, 0);
}, overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
});
testUsingContext('Fails fatally on Vm Service error response', () async {
final ResidentRunner residentWebRunner = setUpResidentRunner(flutterDevice);
fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
...kAttachExpectations,
const FakeVmServiceRequest(
method: 'hotRestart',
// Failed response,
errorCode: RPCErrorCodes.kInternalError,
),
]);
setupMocks();
final Completer<DebugConnectionInfo> connectionInfoCompleter = Completer<DebugConnectionInfo>();
unawaited(residentWebRunner.run(
connectionInfoCompleter: connectionInfoCompleter,
));
await connectionInfoCompleter.future;
final OperationResult result = await residentWebRunner.restart();
expect(result.code, 1);
expect(result.message,
contains(RPCErrorCodes.kInternalError.toString()));
}, overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
});
testUsingContext('printHelp without details shows hot restart help message', () async {
final BufferLogger logger = BufferLogger.test();
final ResidentRunner residentWebRunner = setUpResidentRunner(flutterDevice, logger: logger);
fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[]);
residentWebRunner.printHelp(details: false);
expect(logger.statusText, contains('To hot restart changes'));
}, overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
});
testUsingContext('cleanup of resources is safe to call multiple times', () async {
final ResidentRunner residentWebRunner = setUpResidentRunner(flutterDevice);
mockDevice.dds = DartDevelopmentService();
fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
...kAttachExpectations,
]);
setupMocks();
final Completer<DebugConnectionInfo> connectionInfoCompleter = Completer<DebugConnectionInfo>();
unawaited(residentWebRunner.run(
connectionInfoCompleter: connectionInfoCompleter,
));
await connectionInfoCompleter.future;
await residentWebRunner.exit();
await residentWebRunner.exit();
expect(debugConnection.didClose, false);
expect(fakeVmServiceHost.hasRemainingExpectations, false);
}, overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
});
testUsingContext('cleans up Chrome if tab is closed', () async {
final ResidentRunner residentWebRunner = setUpResidentRunner(flutterDevice);
fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
...kAttachExpectations,
]);
setupMocks();
final Completer<DebugConnectionInfo> connectionInfoCompleter = Completer<DebugConnectionInfo>();
final Future<int> result = residentWebRunner.run(
connectionInfoCompleter: connectionInfoCompleter,
);
await connectionInfoCompleter.future;
debugConnection.completer.complete();
await result;
expect(fakeVmServiceHost.hasRemainingExpectations, false);
}, overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
});
testUsingContext('Prints target and device name on run', () async {
final BufferLogger logger = BufferLogger.test();
final ResidentRunner residentWebRunner = setUpResidentRunner(flutterDevice, logger: logger);
fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
...kAttachExpectations,
]);
setupMocks();
mockDevice.name = 'Chromez';
final Completer<DebugConnectionInfo> connectionInfoCompleter = Completer<DebugConnectionInfo>();
unawaited(residentWebRunner.run(
connectionInfoCompleter: connectionInfoCompleter,
));
await connectionInfoCompleter.future;
expect(logger.statusText, contains(
'Launching ${fileSystem.path.join('lib', 'main.dart')} on '
'Chromez in debug mode',
));
expect(fakeVmServiceHost.hasRemainingExpectations, false);
}, overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
});
testUsingContext('Sends launched app.webLaunchUrl event for Chrome device', () async {
final BufferLogger logger = BufferLogger.test();
fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
...kAttachLogExpectations,
...kAttachIsolateExpectations,
]);
setupMocks();
final FakeChromeConnection chromeConnection = FakeChromeConnection();
final TestChromiumLauncher chromiumLauncher = TestChromiumLauncher();
final Chromium chrome = Chromium(1, chromeConnection, chromiumLauncher: chromiumLauncher);
chromiumLauncher.setInstance(chrome);
flutterDevice.device = GoogleChromeDevice(
fileSystem: fileSystem,
chromiumLauncher: chromiumLauncher,
logger: logger,
platform: FakePlatform(),
processManager: FakeProcessManager.any(),
);
webDevFS.baseUri = Uri.parse('http://localhost:8765/app/');
final FakeChromeTab chromeTab = FakeChromeTab('index.html');
chromeConnection.tabs.add(chromeTab);
final ResidentWebRunner runner = ResidentWebRunner(
flutterDevice,
flutterProject: FlutterProject.fromDirectoryTest(fileSystem.currentDirectory),
debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
ipv6: true,
fileSystem: fileSystem,
logger: logger,
usage: globals.flutterUsage,
systemClock: globals.systemClock,
);
final Completer<DebugConnectionInfo> connectionInfoCompleter = Completer<DebugConnectionInfo>();
unawaited(runner.run(
connectionInfoCompleter: connectionInfoCompleter,
));
await connectionInfoCompleter.future;
// Ensure we got the URL and that it was already launched.
expect(logger.eventText,
contains(json.encode(<String, Object>{
'name': 'app.webLaunchUrl',
'args': <String, Object>{
'url': 'http://localhost:8765/app/',
'launched': true,
},
},
)));
expect(fakeVmServiceHost.hasRemainingExpectations, false);
}, overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
});
testUsingContext('Sends unlaunched app.webLaunchUrl event for Web Server device', () async {
final BufferLogger logger = BufferLogger.test();
fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[]);
setupMocks();
flutterDevice.device = WebServerDevice(
logger: logger,
);
webDevFS.baseUri = Uri.parse('http://localhost:8765/app/');
final ResidentWebRunner runner = ResidentWebRunner(
flutterDevice,
flutterProject: FlutterProject.fromDirectoryTest(fileSystem.currentDirectory),
debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
ipv6: true,
fileSystem: fileSystem,
logger: logger,
usage: globals.flutterUsage,
systemClock: globals.systemClock,
);
final Completer<DebugConnectionInfo> connectionInfoCompleter = Completer<DebugConnectionInfo>();
unawaited(runner.run(
connectionInfoCompleter: connectionInfoCompleter,
));
await connectionInfoCompleter.future;
// Ensure we got the URL and that it was not already launched.
expect(logger.eventText,
contains(json.encode(<String, Object>{
'name': 'app.webLaunchUrl',
'args': <String, Object>{
'url': 'http://localhost:8765/app/',
'launched': false,
},
},
)));
expect(fakeVmServiceHost.hasRemainingExpectations, false);
}, overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
});
// While this file should be ignored on web, generating it here will cause a
// perf regression in hot restart.
testUsingContext('Does not generate dart_plugin_registrant.dart', () async {
// Create necessary files for [DartPluginRegistrantTarget]
final File packageConfig = globals.fs.directory('.dart_tool')
.childFile('package_config.json');
packageConfig.createSync(recursive: true);
packageConfig.writeAsStringSync('''
{
"configVersion": 2,
"packages": [
{
"name": "path_provider_linux",
"rootUri": "../../../path_provider_linux",
"packageUri": "lib/",
"languageVersion": "2.12"
}
]
}
''');
// Start with a dart_plugin_registrant.dart file.
globals.fs.directory('.dart_tool')
.childDirectory('flutter_build')
.childFile('dart_plugin_registrant.dart')
.createSync(recursive: true);
final FlutterProject project = FlutterProject.fromDirectoryTest(fileSystem.currentDirectory);
final ResidentRunner residentWebRunner = setUpResidentRunner(flutterDevice);
await residentWebRunner.runSourceGenerators();
// dart_plugin_registrant.dart should be untouched, indicating that its
// generation didn't run. If it had run, the file would have been removed as
// there are no plugins in the project.
expect(project.dartPluginRegistrant.existsSync(), true);
expect(project.dartPluginRegistrant.readAsStringSync(), '');
}, overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
});
testUsingContext('Successfully turns WebSocketException into ToolExit', () async {
final BufferLogger logger = BufferLogger.test();
final ResidentRunner residentWebRunner = setUpResidentRunner(flutterDevice, logger: logger);
fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[]);
setupMocks();
webDevFS.exception = const WebSocketException();
await expectLater(residentWebRunner.run, throwsToolExit());
expect(logger.errorText, contains('WebSocketException'));
expect(fakeVmServiceHost.hasRemainingExpectations, false);
}, overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
});
testUsingContext('Successfully turns AppConnectionException into ToolExit', () async {
final ResidentRunner residentWebRunner = setUpResidentRunner(flutterDevice);
fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[]);
setupMocks();
webDevFS.exception = AppConnectionException('');
await expectLater(residentWebRunner.run, throwsToolExit());
expect(fakeVmServiceHost.hasRemainingExpectations, false);
}, overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
});
testUsingContext('Successfully turns ChromeDebugError into ToolExit', () async {
final ResidentRunner residentWebRunner = setUpResidentRunner(flutterDevice);
fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[]);
setupMocks();
webDevFS.exception = ChromeDebugException(<String, dynamic>{});
await expectLater(residentWebRunner.run, throwsToolExit());
expect(fakeVmServiceHost.hasRemainingExpectations, false);
}, overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
});
testUsingContext('Rethrows unknown Exception type from dwds', () async {
final ResidentRunner residentWebRunner = setUpResidentRunner(flutterDevice);
fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[]);
setupMocks();
webDevFS.exception = Exception();
await expectLater(residentWebRunner.run, throwsException);
expect(fakeVmServiceHost.hasRemainingExpectations, false);
}, overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
});
testUsingContext('Rethrows unknown Error type from dwds tooling', () async {
final BufferLogger logger = BufferLogger.test();
final ResidentRunner residentWebRunner = setUpResidentRunner(flutterDevice, logger: logger);
fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[]);
setupMocks();
webDevFS.exception = StateError('');
await expectLater(residentWebRunner.run, throwsStateError);
expect(fakeVmServiceHost.hasRemainingExpectations, false);
}, overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
});
}
ResidentRunner setUpResidentRunner(FlutterDevice flutterDevice, {
Logger logger,
SystemClock systemClock,
DebuggingOptions debuggingOptions,
}) {
return ResidentWebRunner(
flutterDevice,
flutterProject: FlutterProject.fromDirectoryTest(globals.fs.currentDirectory),
debuggingOptions: debuggingOptions ?? DebuggingOptions.enabled(BuildInfo.debug),
ipv6: true,
usage: globals.flutterUsage,
systemClock: systemClock ?? SystemClock.fixed(DateTime.now()),
fileSystem: globals.fs,
logger: logger ?? BufferLogger.test(),
devtoolsHandler: createNoOpHandler,
);
}
// Unfortunately Device, despite not being immutable, has an `operator ==`.
// Until we fix that, we have to also ignore related lints here.
// ignore: avoid_implementing_value_types
class FakeWebServerDevice extends FakeDevice implements WebServerDevice { }
// Unfortunately Device, despite not being immutable, has an `operator ==`.
// Until we fix that, we have to also ignore related lints here.
// ignore: avoid_implementing_value_types
class FakeDevice extends Fake implements Device {
@override
String name;
int count = 0;
@override
Future<String> get sdkNameAndVersion async => 'SDK Name and Version';
@override
DartDevelopmentService dds;
@override
Future<LaunchResult> startApp(
covariant ApplicationPackage package, {
String mainPath,
String route,
DebuggingOptions debuggingOptions,
Map<String, dynamic> platformArgs,
bool prebuiltApplication = false,
bool ipv6 = false,
String userIdentifier,
}) async {
return LaunchResult.succeeded();
}
@override
Future<bool> stopApp(
covariant ApplicationPackage app, {
String userIdentifier,
}) async {
if (count > 0) {
throw StateError('stopApp called more than once.');
}
count += 1;
return true;
}
}
class FakeDebugConnection extends Fake implements DebugConnection {
FakeVmServiceHost Function() fakeVmServiceHost;
@override
vm_service.VmService get vmService => fakeVmServiceHost.call().vmService.service;
@override
String uri;
final Completer<void> completer = Completer<void>();
bool didClose = false;
@override
Future<void> get onDone => completer.future;
@override
Future<void> close() async {
didClose = true;
}
}
class FakeAppConnection extends Fake implements AppConnection {
bool ranMain = false;
@override
void runMain() {
ranMain = true;
}
}
// Unfortunately Device, despite not being immutable, has an `operator ==`.
// Until we fix that, we have to also ignore related lints here.
// ignore: avoid_implementing_value_types
class FakeChromeDevice extends Fake implements ChromiumDevice { }
class FakeWipDebugger extends Fake implements WipDebugger { }
class FakeResidentCompiler extends Fake implements ResidentCompiler {
@override
Future<CompilerOutput> recompile(
Uri mainUri,
List<Uri> invalidatedFiles, {
@required String outputPath,
@required PackageConfig packageConfig,
@required String projectRootPath,
@required FileSystem fs,
bool suppressErrors = false,
bool checkDartPluginRegistry = false,
}) async {
return const CompilerOutput('foo.dill', 0, <Uri>[]);
}
@override
void accept() { }
@override
void reset() { }
@override
Future<CompilerOutput> reject() async {
return const CompilerOutput('foo.dill', 0, <Uri>[]);
}
@override
void addFileSystemRoot(String root) { }
}
class FakeWebDevFS extends Fake implements WebDevFS {
Object exception;
ConnectionResult result;
UpdateFSReport report;
Uri mainUri;
@override
List<Uri> sources = <Uri>[];
@override
Uri baseUri = Uri.parse('http://localhost:12345');
@override
DateTime lastCompiled = DateTime.now();
@override
PackageConfig lastPackageConfig = PackageConfig.empty;
@override
Future<Uri> create() async {
return baseUri;
}
@override
Future<UpdateFSReport> update({
@required Uri mainUri,
@required ResidentCompiler generator,
@required bool trackWidgetCreation,
@required String pathToReload,
@required List<Uri> invalidatedFiles,
@required PackageConfig packageConfig,
@required String dillOutputPath,
DevFSWriter devFSWriter,
String target,
AssetBundle bundle,
DateTime firstBuildTime,
bool bundleFirstUpload = false,
bool fullRestart = false,
String projectRootPath,
}) async {
this.mainUri = mainUri;
return report;
}
@override
Future<ConnectionResult> connect(bool useDebugExtension) async {
if (exception != null) {
assert(exception is Exception || exception is Error);
// ignore: only_throw_errors, exception is either Error or Exception here.
throw exception;
}
return result;
}
}
class FakeChromeConnection extends Fake implements ChromeConnection {
final List<ChromeTab> tabs = <ChromeTab>[];
@override
Future<ChromeTab> getTab(bool Function(ChromeTab tab) accept, {Duration retryFor}) async {
return tabs.firstWhere(accept);
}
@override
Future<List<ChromeTab>> getTabs({Duration retryFor}) async {
return tabs;
}
}
class FakeChromeTab extends Fake implements ChromeTab {
FakeChromeTab(this.url);
@override
final String url;
final FakeWipConnection connection = FakeWipConnection();
@override
Future<WipConnection> connect() async {
return connection;
}
}
class FakeWipConnection extends Fake implements WipConnection {
@override
final WipDebugger debugger = FakeWipDebugger();
}
/// A test implementation of the [ChromiumLauncher] that launches a fixed instance.
class TestChromiumLauncher implements ChromiumLauncher {
TestChromiumLauncher();
bool _hasInstance = false;
void setInstance(Chromium chromium) {
_hasInstance = true;
currentCompleter.complete(chromium);
}
@override
Completer<Chromium> currentCompleter = Completer<Chromium>();
@override
bool canFindExecutable() {
return true;
}
@override
Future<Chromium> get connectedInstance => currentCompleter.future;
@override
String findExecutable() {
return 'chrome';
}
@override
bool get hasChromeInstance => _hasInstance;
@override
Future<Chromium> launch(
String url, {
bool headless = false,
int debugPort,
bool skipCheck = false,
Directory cacheDir,
List<String> webBrowserFlags = const <String>[],
}) async {
return currentCompleter.future;
}
@override
Future<Chromium> connect(Chromium chrome, bool skipCheck) {
return currentCompleter.future;
}
}
class FakeFlutterDevice extends Fake implements FlutterDevice {
Uri testUri;
UpdateFSReport report = UpdateFSReport(
success: true,
invalidatedSourcesCount: 1,
);
Exception reportError;
@override
ResidentCompiler generator;
@override
Stream<Uri> get observatoryUris => Stream<Uri>.value(testUri);
@override
FlutterVmService vmService;
DevFS _devFS;
@override
DevFS get devFS => _devFS;
@override
set devFS(DevFS value) { }
@override
Device device;
@override
Future<void> stopEchoingDeviceLog() async { }
@override
Future<void> initLogReader() async { }
@override
Future<Uri> setupDevFS(String fsName, Directory rootDirectory) async {
return testUri;
}
@override
Future<void> exitApps({Duration timeoutDelay = const Duration(seconds: 10)}) async { }
@override
Future<void> connect({
ReloadSources reloadSources,
Restart restart,
CompileExpression compileExpression,
GetSkSLMethod getSkSLMethod,
PrintStructuredErrorLogMethod printStructuredErrorLogMethod,
int hostVmServicePort,
int ddsPort,
bool disableServiceAuthCodes = false,
bool enableDds = true,
bool cacheStartupProfile = false,
@required bool allowExistingDdsInstance,
bool ipv6 = false,
}) async { }
@override
Future<UpdateFSReport> updateDevFS({
Uri mainUri,
String target,
AssetBundle bundle,
DateTime firstBuildTime,
bool bundleFirstUpload = false,
bool bundleDirty = false,
bool fullRestart = false,
String projectRootPath,
String pathToReload,
String dillOutputPath,
List<Uri> invalidatedFiles,
PackageConfig packageConfig,
}) async {
if (reportError != null) {
throw reportError;
}
return report;
}
@override
Future<void> updateReloadStatus(bool wasReloadSuccessful) async { }
}