| // 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/dart2js_compiler_pool.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/util/io.dart'; // ignore: implementation_imports | |
| import 'package:test_core/src/util/package_config.dart'; // ignore: implementation_imports | |
| import 'package:test_core/src/util/stack_trace_mapper.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/package_map.dart'; | |
| import '../../../util/path_handler.dart'; | |
| import 'compiler_support.dart'; | |
| /// Support for Dart2Js compiled tests. | |
| class Dart2JsSupport extends CompilerSupport with JsHtmlWrapper { | |
| /// 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 [Dart2JsCompilerPool] managing active instances of `dart2js`. | |
| final _compilerPool = Dart2JsCompilerPool(); | |
| /// 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/'); | |
| Dart2JsSupport._( | |
| super.config, | |
| super.defaultTemplatePath, | |
| 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<Dart2JsSupport> start({ | |
| required Configuration config, | |
| required String defaultTemplatePath, | |
| required String root, | |
| required String faviconPath, | |
| }) async { | |
| var server = shelf_io.IOServer(await HttpMultiServer.loopback(0)); | |
| return Dart2JsSupport._( | |
| config, | |
| defaultTemplatePath, | |
| 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 jsPath = p.join(dir, '${p.basename(dartPath)}.browser_test.dart.js'); | |
| var bootstrapContent = | |
| ''' | |
| ${suiteConfig.metadata.languageVersionComment ?? await rootPackageLanguageVersionComment} | |
| import 'dart:js_interop'; | |
| import 'package:test/src/bootstrap/browser.dart'; | |
| import 'package:test/src/runner/browser/dom.dart' as dom; | |
| import '${await absoluteUri(dartPath)}' as test; | |
| void main() { | |
| dom.window.console.log(r'Startup for test path $dartPath'.toJS); | |
| internalBootstrapBrowserTest(() => test.main); | |
| } | |
| '''; | |
| await _compilerPool.compile(bootstrapContent, jsPath, suiteConfig); | |
| if (_closed) return; | |
| var bootstrapUrl = | |
| '${p.toUri(p.relative(dartPath, from: _root)).path}' | |
| '.browser_test.dart'; | |
| _pathHandler.add(bootstrapUrl, (request) { | |
| return shelf.Response.ok( | |
| bootstrapContent, | |
| headers: {'Content-Type': 'application/dart'}, | |
| ); | |
| }); | |
| var jsUrl = | |
| '${p.toUri(p.relative(dartPath, from: _root)).path}' | |
| '.browser_test.dart.js'; | |
| _pathHandler.add(jsUrl, (request) { | |
| return shelf.Response.ok( | |
| File(jsPath).readAsStringSync(), | |
| headers: {'Content-Type': 'application/javascript'}, | |
| ); | |
| }); | |
| var mapUrl = | |
| '${p.toUri(p.relative(dartPath, from: _root)).path}' | |
| '.browser_test.dart.js.map'; | |
| _pathHandler.add(mapUrl, (request) { | |
| return shelf.Response.ok( | |
| File('$jsPath.map').readAsStringSync(), | |
| headers: {'Content-Type': 'application/json'}, | |
| ); | |
| }); | |
| if (suiteConfig.jsTrace) return; | |
| var mapPath = '$jsPath.map'; | |
| _mappers[dartPath] = JSStackTraceMapper( | |
| File(mapPath).readAsStringSync(), | |
| mapUrl: p.toUri(mapPath), | |
| sdkRoot: Uri.parse('org-dartlang-sdk:///sdk'), | |
| packageMap: (await currentPackageConfig).toPackageMap(), | |
| ); | |
| }); | |
| } | |
| @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); | |
| } | |
| } |