blob: 2ca1d04e483b5345325234b99735ec95e7510e3b [file] [log] [blame]
// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
// for details. 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.9
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:build_daemon/client.dart';
import 'package:build_daemon/data/build_status.dart';
import 'package:build_daemon/data/build_target.dart';
import 'package:build_daemon/data/server_log.dart';
import 'package:dwds/dwds.dart';
import 'package:dwds/src/debugging/webkit_debugger.dart';
import 'package:dwds/src/utilities/dart_uri.dart';
import 'package:dwds/src/utilities/shared.dart';
import 'package:frontend_server_common/src/resident_runner.dart';
import 'package:http/http.dart';
import 'package:http/io_client.dart';
import 'package:logging/logging.dart' as logging;
import 'package:path/path.dart' as p;
import 'package:shelf/shelf.dart';
import 'package:shelf_proxy/shelf_proxy.dart';
import 'package:test/test.dart';
import 'package:vm_service/vm_service.dart';
import 'package:webdriver/io.dart';
import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';
import 'logging.dart';
import 'server.dart';
import 'utilities.dart';
final _batExt = Platform.isWindows ? '.bat' : '';
final _exeExt = Platform.isWindows ? '.exe' : '';
const isRPCError = TypeMatcher<RPCError>();
const isSentinelException = TypeMatcher<SentinelException>();
final Matcher throwsRPCError = throwsA(isRPCError);
final Matcher throwsSentinelException = throwsA(isSentinelException);
enum CompilationMode { buildDaemon, frontendServer }
class TestContext {
String appUrl;
WipConnection tabConnection;
WipConnection extensionConnection;
TestServer testServer;
BuildDaemonClient daemonClient;
ResidentWebRunner webRunner;
WebDriver webDriver;
Process chromeDriver;
AppConnection appConnection;
DebugConnection debugConnection;
WebkitDebugger webkitDebugger;
Client client;
ExpressionCompilerService ddcService;
int port;
Directory _outputDir;
File _entryFile;
String _packagesFilePath;
String _entryContents;
/// Null safety mode for the frontend server.
///
/// Note: flutter's frontend server is always launched with
/// the null safety setting inferred from project configurations
/// or the source code. We skip this inference and just set it
/// here to the desired value manually.
///
/// Note: build_runner-based setups ignore this setting and read
/// this value from the ddc debug metadata and pass it to the
/// expression compiler worker initialiation API.
bool soundNullSafety;
final _logger = logging.Logger('TestContext');
/// Top level directory in which we run the test server..
String workingDirectory;
/// The path to build and serve.
String pathToServe;
/// The path part of the application URL.
String path;
TestContext(
{String directory,
String entry,
this.path = 'hello_world/index.html',
this.pathToServe = 'example'}) {
var relativeDirectory = p.join('..', 'fixtures', '_test');
var relativeEntry = p.join(
'..', 'fixtures', '_test', 'example', 'append_body', 'main.dart');
workingDirectory = p.normalize(p
.absolute(directory ?? p.relative(relativeDirectory, from: p.current)));
DartUri.currentDirectory = workingDirectory;
_packagesFilePath = p.join(workingDirectory, '.packages');
_entryFile = File(p.normalize(
p.absolute(entry ?? p.relative(relativeEntry, from: p.current))));
_entryContents = _entryFile.readAsStringSync();
}
Future<void> setUp(
{ReloadConfiguration reloadConfiguration,
bool serveDevTools,
bool enableDebugExtension,
bool autoRun,
bool enableDebugging,
bool useSse,
bool spawnDds,
String hostname,
bool waitToDebug,
UrlEncoder urlEncoder,
bool restoreBreakpoints,
CompilationMode compilationMode,
bool enableExpressionEvaluation,
bool verbose}) async {
reloadConfiguration ??= ReloadConfiguration.none;
serveDevTools ??= false;
enableDebugExtension ??= false;
autoRun ??= true;
enableDebugging ??= true;
waitToDebug ??= false;
compilationMode ??= CompilationMode.buildDaemon;
enableExpressionEvaluation ??= false;
spawnDds ??= true;
verbose ??= false;
try {
configureLogWriter();
client = IOClient(HttpClient()
..maxConnectionsPerHost = 200
..idleTimeout = const Duration(seconds: 30)
..connectionTimeout = const Duration(seconds: 30));
var systemTempDir = Directory.systemTemp;
_outputDir = systemTempDir.createTempSync('foo bar');
var chromeDriverPort = await findUnusedPort();
var chromeDriverUrlBase = 'wd/hub';
try {
chromeDriver = await Process.start('chromedriver$_exeExt',
['--port=$chromeDriverPort', '--url-base=$chromeDriverUrlBase']);
// On windows this takes a while to boot up, wait for the first line
// of stdout as a signal that it is ready.
final stdOutLines = chromeDriver.stdout
.transform(utf8.decoder)
.transform(const LineSplitter())
.asBroadcastStream();
final stdErrLines = chromeDriver.stderr
.transform(utf8.decoder)
.transform(const LineSplitter())
.asBroadcastStream();
stdOutLines
.listen((line) => _logger.finest('ChromeDriver stdout: $line'));
stdErrLines
.listen((line) => _logger.warning('ChromeDriver stderr: $line'));
await stdOutLines.first;
} catch (e) {
throw StateError(
'Could not start ChromeDriver. Is it installed?\nError: $e');
}
await Process.run('pub$_batExt', ['upgrade'],
workingDirectory: workingDirectory);
ExpressionCompiler expressionCompiler;
AssetReader assetReader;
Handler assetHandler;
Stream<BuildResults> buildResults;
RequireStrategy requireStrategy;
port = await findUnusedPort();
switch (compilationMode) {
case CompilationMode.buildDaemon:
{
var options = [
if (enableExpressionEvaluation) ...[
'--define',
'build_web_compilers|ddc=generate-full-dill=true',
],
if (verbose) '--verbose',
];
daemonClient = await connectClient(workingDirectory, options,
(log) => _logger.log(toLoggingLevel(log.level), log.message));
daemonClient.registerBuildTarget(
DefaultBuildTarget((b) => b..target = pathToServe));
daemonClient.startBuild();
await daemonClient.buildResults
.firstWhere((results) => results.results
.any((result) => result.status == BuildStatus.succeeded))
.timeout(const Duration(seconds: 60));
var assetServerPort = daemonPort(workingDirectory);
assetHandler = proxyHandler(
'http://localhost:$assetServerPort/$pathToServe/',
client: client);
assetReader =
ProxyServerAssetReader(assetServerPort, root: pathToServe);
if (enableExpressionEvaluation) {
ddcService = ExpressionCompilerService(
'localhost',
port,
assetHandler,
verbose,
);
expressionCompiler = ddcService;
}
requireStrategy = BuildRunnerRequireStrategyProvider(
assetHandler,
reloadConfiguration,
assetReader,
).strategy;
buildResults = daemonClient.buildResults;
}
break;
case CompilationMode.frontendServer:
{
soundNullSafety ??= true;
var fileSystemRoot = p.dirname(_packagesFilePath);
var entryPath =
_entryFile.path.substring(fileSystemRoot.length + 1);
webRunner = ResidentWebRunner(
'${Uri.file(entryPath)}',
urlEncoder,
fileSystemRoot,
_packagesFilePath,
[fileSystemRoot],
'org-dartlang-app',
_outputDir.path,
verbose);
var assetServerPort = await findUnusedPort();
await webRunner.run(hostname, assetServerPort, pathToServe);
if (enableExpressionEvaluation) {
expressionCompiler = webRunner.expressionCompiler;
}
assetReader = webRunner.devFS.assetServer;
assetHandler = webRunner.devFS.assetServer.handleRequest;
requireStrategy = FrontendServerRequireStrategyProvider(
reloadConfiguration, assetReader, () async => {}).strategy;
buildResults = const Stream<BuildResults>.empty();
}
break;
default:
throw Exception('Unsupported compilation mode: $compilationMode');
}
var debugPort = await findUnusedPort();
// If the environment variable DWDS_DEBUG_CHROME is set to the string true
// then Chrome will be launched with a UI rather than headless.
// If the extension is enabled, then Chrome will be launched with a UI
// since headless Chrome does not support extensions.
var headless = Platform.environment['DWDS_DEBUG_CHROME'] != 'true' &&
!enableDebugExtension;
var capabilities = Capabilities.chrome
..addAll({
Capabilities.chromeOptions: {
'args': [
'remote-debugging-port=$debugPort',
if (enableDebugExtension) '--load-extension=debug_extension/web',
if (headless) '--headless'
]
}
});
webDriver = await createDriver(
spec: WebDriverSpec.JsonWire,
desired: capabilities,
uri: Uri.parse(
'http://127.0.0.1:$chromeDriverPort/$chromeDriverUrlBase/'));
var connection = ChromeConnection('localhost', debugPort);
testServer = await TestServer.start(
hostname,
port,
assetHandler,
assetReader,
requireStrategy,
pathToServe,
buildResults,
() async => connection,
serveDevTools,
enableDebugExtension,
autoRun,
enableDebugging,
useSse,
urlEncoder,
restoreBreakpoints,
expressionCompiler,
spawnDds,
ddcService,
);
appUrl = 'http://localhost:$port/$path';
await webDriver.get(appUrl);
var tab = await connection.getTab((t) => t.url == appUrl);
tabConnection = await tab.connect();
await tabConnection.runtime.enable();
await tabConnection.debugger.enable();
if (enableDebugExtension) {
var extensionTab = await _fetchDartDebugExtensionTab(connection);
extensionConnection = await extensionTab.connect();
await extensionConnection.runtime.enable();
}
appConnection = await testServer.dwds.connectedApps.first;
if (enableDebugging && !waitToDebug) {
await startDebugging();
}
} catch (e) {
await tearDown();
rethrow;
}
}
Future<void> startDebugging() async {
debugConnection = await testServer.dwds.debugConnection(appConnection);
webkitDebugger = WebkitDebugger(WipDebugger(tabConnection));
}
Future<Null> tearDown() async {
await webDriver?.quit(closeSession: true);
chromeDriver?.kill();
DartUri.currentDirectory = p.current;
_entryFile.writeAsStringSync(_entryContents);
await daemonClient?.close();
await ddcService?.stop();
await webRunner?.stop();
await testServer?.stop();
client?.close();
await _outputDir?.delete(recursive: true);
stopLogWriter();
// clear the state for next setup
webDriver = null;
chromeDriver = null;
daemonClient = null;
ddcService = null;
webRunner = null;
testServer = null;
client = null;
_outputDir = null;
}
Future<void> changeInput() async {
_entryFile.writeAsStringSync(
_entryContents.replaceAll('Hello World!', 'Gary is awesome!'));
// Wait for the build.
await daemonClient.buildResults.firstWhere((results) => results.results
.any((result) => result.status == BuildStatus.succeeded));
// Allow change to propagate to the browser.
// Windows, or at least Travis on Windows, seems to need more time.
var delay = Platform.isWindows
? const Duration(seconds: 5)
: const Duration(seconds: 2);
await Future.delayed(delay);
}
Future<ChromeTab> _fetchDartDebugExtensionTab(
ChromeConnection connection) async {
var extensionTabs = (await connection.getTabs()).where((tab) {
return tab.isChromeExtension;
});
for (var tab in extensionTabs) {
var tabConnection = await tab.connect();
var response =
await tabConnection.runtime.evaluate('window.isDartDebugExtension');
if (response.value == true) {
return tab;
}
}
throw StateError('No extension installed.');
}
/// Finds the line number in [scriptRef] matching [breakpointId].
///
/// A breakpoint ID is found by looking for a line that ends with a comment
/// of exactly this form: `// Breakpoint: <id>`.
///
/// Throws if it can't find the matching line.
Future<int> findBreakpointLine(
String breakpointId, String isolateId, ScriptRef scriptRef) async {
var script = await debugConnection.vmService
.getObject(isolateId, scriptRef.id) as Script;
var lines = LineSplitter.split(script.source).toList();
var lineNumber =
lines.indexWhere((l) => l.endsWith('// Breakpoint: $breakpointId'));
if (lineNumber == -1) {
throw StateError('Unable to find breakpoint in ${scriptRef.uri} with id '
'$breakpointId');
}
return lineNumber + 1;
}
}