Skip to content

Promote dart2wasm compiler support to the stable browser platform. #2144

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Dec 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion integration_tests/wasm/dart_test.yaml
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
platforms: [experimental-chrome-wasm]
platforms: [chrome, firefox]
compilers: [dart2wasm]
1 change: 0 additions & 1 deletion integration_tests/wasm/test/hello_world_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// 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.

@Skip('https://github.com/dart-lang/sdk/issues/53493')
@TestOn('wasm')
// This retry is a regression test for https://github.com/dart-lang/test/issues/2006
@Retry(2)
Expand Down
13 changes: 12 additions & 1 deletion pkgs/test/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
## 1.24.10-wip
## 1.25.0-wip

* Handle paths with leading `/` when spawning test isolates.
* Add support for the `dart2wasm` compiler in chrome and firefox.
* **BREAKING**: Remove the `experimental-chrome-wasm` platform, you can now use
`-p chrome -c dart2wasm` instead.
* Note that this has always been advertised as a change that would happen in a
future non-breaking release.
* **BREAKING**:Dropped support for `--pub-serve` which has long not been tested
or supported.
* We do not anticipate much if any actual breakage or existing usage of this
feature, which is why we are making this change in a non-breaking release.
* If you do require this feature, file an issue and we can look at adding it
back.

## 1.24.9

Expand Down
13 changes: 0 additions & 13 deletions pkgs/test/doc/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ tags:
* [`concurrency`](#concurrency)
* [`pause_after_load`](#pause_after_load)
* [`run_skipped`](#run_skipped)
* [`pub_serve`](#pub_serve)
* [`reporter`](#reporter)
* [`file_reporters`](#file_reporters)
* [`fold_stack_frames`](#fold_stack_frames)
Expand Down Expand Up @@ -475,17 +474,6 @@ presets:
paths: ["test/", "extra_test/"]
```

### `pub_serve`

This field indicates that the test runner should run against a `pub serve`
instance by default, and provides the port number for that instance. Note that
if there is no `pub serve` instance running at that port, running the tests will
fail by default.

```yaml
pub_serve: 8081
```

### `reporter`

This field indicates the default reporter to use. It may be set to "compact",
Expand Down Expand Up @@ -855,7 +843,6 @@ presets:
browser:
paths:
- test/runner/browser
- test/runner/pub_serve_test.dart
```

The `presets` field counts as [test configuration](#test-configuration). It can
Expand Down
4 changes: 0 additions & 4 deletions pkgs/test/lib/src/executable.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import 'package:test_core/src/runner/hack_register_platform.dart'; // ignore: im

import 'runner/browser/platform.dart';
import 'runner/node/platform.dart';
import 'runner/wasm/platform.dart';

Future<void> main(List<String> args) async {
registerPlatformPlugin([Runtime.nodeJS], NodePlatform.new);
Expand All @@ -20,9 +19,6 @@ Future<void> main(List<String> args) async {
Runtime.safari,
Runtime.internetExplorer
], BrowserPlatform.start);
registerPlatformPlugin([
Runtime.experimentalChromeWasm,
], BrowserWasmPlatform.start);

await executable.main(args);
}
4 changes: 1 addition & 3 deletions pkgs/test/lib/src/runner/browser/browser_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -155,9 +155,7 @@ class BrowserManager {
static Browser _newBrowser(Uri url, Runtime browser,
ExecutableSettings settings, Configuration configuration) =>
switch (browser.root) {
Runtime.chrome ||
Runtime.experimentalChromeWasm =>
Chrome(url, configuration, settings: settings),
Runtime.chrome => Chrome(url, configuration, settings: settings),
Runtime.firefox => Firefox(url, settings: settings),
Runtime.safari => Safari(url, settings: settings),
Runtime.internetExplorer => InternetExplorer(url, settings: settings),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// 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 'package:test_api/backend.dart' show StackTraceMapper, SuitePlatform;
import 'package:test_core/src/runner/suite.dart'; // ignore: implementation_imports
import 'package:web_socket_channel/web_socket_channel.dart'; // ignore: implementation_imports

/// The shared interface for all compiler support libraries.
abstract interface class CompilerSupport {
/// The URL at which this compiler serves its tests.
///
/// Each compiler serves its tests under a different directory.
Uri get serverUrl;

/// Compiles [dartPath] using [suiteConfig] for [platform].
///
/// [dartPath] is the path to the original `.dart` test suite, relative to the
/// package root.
Future<void> compileSuite(
String dartPath, SuiteConfiguration suiteConfig, SuitePlatform platform);

/// Retrieves a stack trace mapper for [dartPath] if available.
///
/// [dartPath] is the path to the original `.dart` test suite, relative to the
/// package root.
StackTraceMapper? stackTraceMapperForPath(String dartPath);

/// Returns the eventual URI for the web socket, as well as the channel itself
/// once the connection is established.
(Uri uri, Future<WebSocketChannel> socket) get webSocket;

/// Closes down anything necessary for this implementation.
Future<void> close();
}
208 changes: 208 additions & 0 deletions pkgs/test/lib/src/runner/browser/compilers/dart2js.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
// 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:convert';
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 implements CompilerSupport {
/// 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();

/// The global test runner configuration.
final Configuration _config;

/// The default template path.
final String _defaultTemplatePath;

/// 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._(this._config, this._defaultTemplatePath, this._server,
this._root, String faviconPath) {
var cascade = shelf.Cascade()
.add(_webSocketHandler.handler)
.add(packagesDirHandler())
.add(_pathHandler.handler)
.add(createStaticHandler(_root))
.add(_wrapperHandler);

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);
}

/// A handler that serves wrapper files used to bootstrap tests.
shelf.Response _wrapperHandler(shelf.Request request) {
var path = p.fromUri(request.url);

if (path.endsWith('.html')) {
var test = p.setExtension(path, '.dart');
var scriptBase = htmlEscape.convert(p.basename(test));
var link = '<link rel="x-dart-test" href="$scriptBase">';
var testName = htmlEscape.convert(test);
var template = _config.customHtmlTemplatePath ?? _defaultTemplatePath;
var contents = File(template).readAsStringSync();
var processedContents = contents
// Checked during loading phase that there is only one {{testScript}} placeholder.
.replaceFirst('{{testScript}}', link)
.replaceAll('{{testName}}', testName);
return shelf.Response.ok(processedContents,
headers: {'Content-Type': 'text/html'});
}

return shelf.Response.notFound('Not found.');
}

@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 "package:test/src/bootstrap/browser.dart";

import "${p.toUri(p.absolute(dartPath))}" as test;

void main() {
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();
var path = _webSocketHandler.create(webSocketHandler(completer.complete));
var webSocketUrl = serverUrl.replace(scheme: 'ws').resolve(path);
return (webSocketUrl, completer.future);
}
}
Loading