blob: 31ad66152fad024c47ce06aeb9b8881be5db4629 [file] [log] [blame]
// Copyright (c) 2023, 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.
import 'dart:async';
import 'dart:io';
import 'package:http_multi_server/http_multi_server.dart';
import 'package:path/path.dart' as p;
import 'package:shelf/shelf.dart' as shelf;
import 'package:shelf/shelf_io.dart' as shelf_io;
import 'package:shelf_packages_handler/shelf_packages_handler.dart';
import 'package:shelf_static/shelf_static.dart';
import 'package:shelf_web_socket/shelf_web_socket.dart';
import 'package:test_api/backend.dart' show StackTraceMapper, SuitePlatform;
import 'package:test_core/src/runner/configuration.dart'; // ignore: implementation_imports
import 'package:test_core/src/runner/package_version.dart'; // ignore: implementation_imports
import 'package:test_core/src/runner/suite.dart'; // ignore: implementation_imports
import 'package:test_core/src/runner/wasm_compiler_pool.dart'; // ignore: implementation_imports
import 'package:test_core/src/util/io.dart'; // ignore: implementation_imports
import 'package:test_core/src/util/package_config.dart'; // ignore: implementation_imports
import 'package:web_socket_channel/web_socket_channel.dart';
import '../../../util/math.dart';
import '../../../util/one_off_handler.dart';
import '../../../util/path_handler.dart';
import '../browser_manager.dart';
import 'compiler_support.dart';
/// Support for Dart2Wasm compiled tests.
class Dart2WasmSupport extends CompilerSupport with WasmHtmlWrapper {
/// Whether [close] has been called.
bool _closed = false;
/// The temporary directory in which compiled JS is emitted.
final _compiledDir = createTempDir();
/// A map from test suite paths to Futures that will complete once those
/// suites are finished compiling.
///
/// This is used to make sure that a given test suite is only compiled once
/// per run, rather than once per browser per run.
final _compileFutures = <String, Future<void>>{};
/// The [WasmCompilerPool] managing active instances of `dart2wasm`.
final _compilerPool = WasmCompilerPool();
/// The `package:test` side wrapper for the Dart2Wasm runtime.
final String _jsRuntimeWrapper;
/// Mappers for Dartifying stack traces, indexed by test path.
final _mappers = <String, StackTraceMapper>{};
/// A [PathHandler] used to serve test specific artifacts.
final _pathHandler = PathHandler();
/// The root directory served statically by this server.
final String _root;
/// Each compiler serves its tests under a different randomly-generated
/// secret URI to ensure that other users on the same system can't snoop
/// on data being served through this server, as well as distinguish tests
/// from different compilers from each other.
final String _secret = randomUrlSecret();
/// The underlying server.
final shelf.Server _server;
/// A [OneOffHandler] for servicing WebSocket connections for
/// [BrowserManager]s.
///
/// This is one-off because each [BrowserManager] can only connect to a single
/// WebSocket.
final _webSocketHandler = OneOffHandler();
@override
Uri get serverUrl => _server.url.resolve('$_secret/');
Dart2WasmSupport._(super.config, super.defaultTemplatePath,
this._jsRuntimeWrapper, this._server, this._root, String faviconPath) {
var cascade = shelf.Cascade()
.add(_webSocketHandler.handler)
.add(packagesDirHandler())
.add(_pathHandler.handler)
.add(createStaticHandler(_root))
.add(htmlWrapperHandler);
var pipeline = const shelf.Pipeline()
.addMiddleware(PathHandler.nestedIn(_secret))
.addHandler(cascade.handler);
_server.mount(shelf.Cascade()
.add(createFileHandler(faviconPath))
.add(pipeline)
.handler);
}
static Future<Dart2WasmSupport> start({
required Configuration config,
required String defaultTemplatePath,
required String jsRuntimeWrapper,
required String root,
required String faviconPath,
}) async {
var server = shelf_io.IOServer(await HttpMultiServer.loopback(0));
return Dart2WasmSupport._(config, defaultTemplatePath, jsRuntimeWrapper,
server, root, faviconPath);
}
@override
Future<void> compileSuite(
String dartPath, SuiteConfiguration suiteConfig, SuitePlatform platform) {
return _compileFutures.putIfAbsent(dartPath, () async {
var dir = Directory(_compiledDir).createTempSync('test_').path;
var baseCompiledPath =
p.join(dir, '${p.basename(dartPath)}.browser_test.dart');
var baseUrl =
'${p.toUri(p.relative(dartPath, from: _root)).path}.browser_test.dart';
var wasmUrl = '$baseUrl.wasm';
var jsRuntimeWrapperUrl = '$baseUrl.js';
var jsRuntimeUrl = '$baseUrl.mjs';
var htmlUrl = '$baseUrl.html';
var bootstrapContent = '''
${suiteConfig.metadata.languageVersionComment ?? await rootPackageLanguageVersionComment}
import 'package:test/src/bootstrap/browser.dart';
import '${await absoluteUri(dartPath)}' as test;
void main() {
internalBootstrapBrowserTest(() => test.main);
}
''';
await _compilerPool.compile(
bootstrapContent, baseCompiledPath, suiteConfig);
if (_closed) return;
var wasmPath = '$baseCompiledPath.wasm';
_pathHandler.add(wasmUrl, (request) {
return shelf.Response.ok(File(wasmPath).readAsBytesSync(),
headers: {'Content-Type': 'application/wasm'});
});
_pathHandler.add(jsRuntimeWrapperUrl, (request) {
return shelf.Response.ok(File(_jsRuntimeWrapper).readAsBytesSync(),
headers: {'Content-Type': 'application/javascript'});
});
var jsRuntimePath = '$baseCompiledPath.mjs';
_pathHandler.add(jsRuntimeUrl, (request) {
return shelf.Response.ok(File(jsRuntimePath).readAsBytesSync(),
headers: {'Content-Type': 'application/javascript'});
});
var htmlPath = '$baseCompiledPath.html';
_pathHandler.add(htmlUrl, (request) {
return shelf.Response.ok(File(htmlPath).readAsBytesSync(),
headers: {'Content-Type': 'text/html'});
});
});
}
@override
Future<void> close() async {
if (_closed) return;
_closed = true;
await Future.wait([
Directory(_compiledDir).deleteWithRetry(),
_compilerPool.close(),
_server.close(),
]);
}
@override
StackTraceMapper? stackTraceMapperForPath(String dartPath) =>
_mappers[dartPath];
@override
(Uri, Future<WebSocketChannel>) get webSocket {
var completer = Completer<WebSocketChannel>.sync();
// Note: the WebSocketChannel type below is needed for compatibility with
// package:shelf_web_socket v2.
var path =
_webSocketHandler.create(webSocketHandler((WebSocketChannel ws, _) {
completer.complete(ws);
}));
var webSocketUrl = serverUrl.replace(scheme: 'ws').resolve(path);
return (webSocketUrl, completer.future);
}
}